File indexing completed on 2024-05-12 03:54:26

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2006, 2007 Thomas Braxton <kde.braxton@gmail.com>
0004     SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org>
0005     SPDX-FileCopyrightText: 1997-1999 Matthias Kalle Dalheimer <kalle@kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "kconfigini_p.h"
0011 
0012 #include "bufferfragment_p.h"
0013 #include "kconfig.h"
0014 #include "kconfig_core_log_settings.h"
0015 #include "kconfigbackend_p.h"
0016 #include "kconfigdata_p.h"
0017 
0018 #include <QDateTime>
0019 #include <QDebug>
0020 #include <QDir>
0021 #include <QFile>
0022 #include <QFileInfo>
0023 #include <QLockFile>
0024 #include <QSaveFile>
0025 #include <qplatformdefs.h>
0026 
0027 #ifndef Q_OS_WIN
0028 #include <unistd.h> // getuid, close
0029 #endif
0030 #include <fcntl.h> // open
0031 #include <sys/types.h> // uid_t
0032 
0033 KCONFIGCORE_EXPORT bool kde_kiosk_exception = false; // flag to disable kiosk restrictions
0034 
0035 static QByteArray lookup(const KConfigIniBackend::BufferFragment fragment, QHash<KConfigIniBackend::BufferFragment, QByteArray> *cache)
0036 {
0037     auto it = cache->constFind(fragment);
0038     if (it != cache->constEnd()) {
0039         return it.value();
0040     }
0041 
0042     return cache->insert(fragment, fragment.toByteArray()).value();
0043 }
0044 
0045 QString KConfigIniBackend::warningProlog(const QFile &file, int line)
0046 {
0047     // %2 then %1 i.e. int before QString, so that the QString is last
0048     // This avoids a wrong substitution if the fileName itself contains %1
0049     return QStringLiteral("KConfigIni: In file %2, line %1:").arg(line).arg(file.fileName());
0050 }
0051 
0052 KConfigIniBackend::KConfigIniBackend()
0053     : KConfigBackend()
0054     , lockFile(nullptr)
0055 {
0056 }
0057 
0058 KConfigIniBackend::~KConfigIniBackend()
0059 {
0060 }
0061 
0062 KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options)
0063 {
0064     return parseConfig(currentLocale, entryMap, options, false);
0065 }
0066 
0067 // merging==true is the merging that happens at the beginning of writeConfig:
0068 // merge changes in the on-disk file with the changes in the KConfig object.
0069 KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options, bool merging)
0070 {
0071     if (filePath().isEmpty()) {
0072         return ParseOk;
0073     }
0074 
0075     QFile file(filePath());
0076     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0077         return file.exists() ? ParseOpenError : ParseOk;
0078     }
0079 
0080     QList<QString> immutableGroups;
0081 
0082     bool fileOptionImmutable = false;
0083     bool groupOptionImmutable = false;
0084     bool groupSkip = false;
0085 
0086     int lineNo = 0;
0087     // on systems using \r\n as end of line, \r will be taken care of by
0088     // trim() below
0089     QByteArray buffer = file.readAll();
0090     BufferFragment contents(buffer.data(), buffer.size());
0091     unsigned int len = contents.length();
0092     unsigned int startOfLine = 0;
0093 
0094     const int langIdx = currentLocale.indexOf('_');
0095     const QByteArray currentLanguage = langIdx >= 0 ? currentLocale.left(langIdx) : currentLocale;
0096 
0097     QString currentGroup = QStringLiteral("<default>");
0098     bool bDefault = options & ParseDefaults;
0099     bool allowExecutableValues = options & ParseExpansions;
0100 
0101     // Reduce memory overhead by making use of implicit sharing
0102     // This assumes that config files contain only a small amount of
0103     // different fragments which are repeated often.
0104     // This is often the case, especially sub groups will all have
0105     // the same list of keys and similar values as well.
0106     QHash<BufferFragment, QByteArray> cache;
0107     cache.reserve(4096);
0108 
0109     while (startOfLine < len) {
0110         BufferFragment line = contents.split('\n', &startOfLine);
0111         line.trim();
0112         ++lineNo;
0113 
0114         // skip empty lines and lines beginning with '#'
0115         if (line.isEmpty() || line.at(0) == '#') {
0116             continue;
0117         }
0118 
0119         if (line.at(0) == '[') { // found a group
0120             groupOptionImmutable = fileOptionImmutable;
0121 
0122             QByteArray newGroup;
0123             int start = 1;
0124             int end = 0;
0125             do {
0126                 end = start;
0127                 for (;;) {
0128                     if (end == line.length()) {
0129                         qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid group header.";
0130                         // XXX maybe reset the current group here?
0131                         goto next_line;
0132                     }
0133                     if (line.at(end) == ']') {
0134                         break;
0135                     }
0136                     ++end;
0137                 }
0138                 /* clang-format off */
0139                 if (end + 1 == line.length()
0140                     && start + 2 == end
0141                     && line.at(start) == '$'
0142                     && line.at(start + 1) == 'i') { /* clang-format on */
0143                     if (newGroup.isEmpty()) {
0144                         fileOptionImmutable = !kde_kiosk_exception;
0145                     } else {
0146                         groupOptionImmutable = !kde_kiosk_exception;
0147                     }
0148                 } else {
0149                     if (!newGroup.isEmpty()) {
0150                         newGroup += '\x1d';
0151                     }
0152                     BufferFragment namePart = line.mid(start, end - start);
0153                     printableToString(&namePart, file, lineNo);
0154                     newGroup += namePart.toByteArray();
0155                 }
0156             } while ((start = end + 2) <= line.length() && line.at(end + 1) == '[');
0157             currentGroup = QString::fromUtf8(newGroup);
0158 
0159             groupSkip = entryMap.getEntryOption(currentGroup, {}, {}, KEntryMap::EntryImmutable);
0160 
0161             if (groupSkip && !bDefault) {
0162                 continue;
0163             }
0164 
0165             if (groupOptionImmutable)
0166             // Do not make the groups immutable until the entries from
0167             // this file have been added.
0168             {
0169                 immutableGroups.append(currentGroup);
0170             }
0171         } else {
0172             if (groupSkip && !bDefault) {
0173                 continue; // skip entry
0174             }
0175 
0176             BufferFragment aKey;
0177             int eqpos = line.indexOf('=');
0178             if (eqpos < 0) {
0179                 aKey = line;
0180                 line.clear();
0181             } else {
0182                 BufferFragment temp = line.left(eqpos);
0183                 temp.trim();
0184                 aKey = temp;
0185                 line.truncateLeft(eqpos + 1);
0186                 line.trim();
0187             }
0188             if (aKey.isEmpty()) {
0189                 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (empty key)";
0190                 continue;
0191             }
0192 
0193             KEntryMap::EntryOptions entryOptions = {};
0194             if (groupOptionImmutable) {
0195                 entryOptions |= KEntryMap::EntryImmutable;
0196             }
0197 
0198             BufferFragment locale;
0199             int start;
0200             while ((start = aKey.lastIndexOf('[')) >= 0) {
0201                 int end = aKey.indexOf(']', start);
0202                 if (end < 0) {
0203                     qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (missing ']')";
0204                     goto next_line;
0205                 } else if (end > start + 1 && aKey.at(start + 1) == '$') { // found option(s)
0206                     int i = start + 2;
0207                     while (i < end) {
0208                         switch (aKey.at(i)) {
0209                         case 'i':
0210                             if (!kde_kiosk_exception) {
0211                                 entryOptions |= KEntryMap::EntryImmutable;
0212                             }
0213                             break;
0214                         case 'e':
0215                             if (allowExecutableValues) {
0216                                 entryOptions |= KEntryMap::EntryExpansion;
0217                             }
0218                             break;
0219                         case 'd':
0220                             entryOptions |= KEntryMap::EntryDeleted;
0221                             aKey.truncate(start);
0222                             printableToString(&aKey, file, lineNo);
0223                             entryMap.setEntry(currentGroup, aKey.toByteArray(), QByteArray(), entryOptions);
0224                             goto next_line;
0225                         default:
0226                             break;
0227                         }
0228                         ++i;
0229                     }
0230                 } else { // found a locale
0231                     if (!locale.isNull()) {
0232                         qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (second locale!?)";
0233                         goto next_line;
0234                     }
0235 
0236                     locale = aKey.mid(start + 1, end - start - 1);
0237                 }
0238                 aKey.truncate(start);
0239             }
0240             if (eqpos < 0) { // Do this here after [$d] was checked
0241                 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (missing '=')";
0242                 continue;
0243             }
0244             printableToString(&aKey, file, lineNo);
0245             if (!locale.isEmpty()) {
0246                 if (locale != currentLocale && locale != currentLanguage) {
0247                     // backward compatibility. C == en_US
0248                     if (locale.at(0) != 'C' || currentLocale != "en_US") {
0249                         if (merging) {
0250                             entryOptions |= KEntryMap::EntryRawKey;
0251                         } else {
0252                             goto next_line; // skip this entry if we're not merging
0253                         }
0254                     }
0255                 }
0256             }
0257 
0258             if (!(entryOptions & KEntryMap::EntryRawKey)) {
0259                 printableToString(&aKey, file, lineNo);
0260             }
0261 
0262             if (options & ParseGlobal) {
0263                 entryOptions |= KEntryMap::EntryGlobal;
0264             }
0265             if (bDefault) {
0266                 entryOptions |= KEntryMap::EntryDefault;
0267             }
0268             if (!locale.isNull()) {
0269                 entryOptions |= KEntryMap::EntryLocalized;
0270                 if (locale.indexOf('_') != -1) {
0271                     entryOptions |= KEntryMap::EntryLocalizedCountry;
0272                 }
0273             }
0274             printableToString(&line, file, lineNo);
0275             if (entryOptions & KEntryMap::EntryRawKey) {
0276                 QByteArray rawKey;
0277                 rawKey.reserve(aKey.length() + locale.length() + 2);
0278                 rawKey.append(aKey.toVolatileByteArray());
0279                 rawKey.append('[').append(locale.toVolatileByteArray()).append(']');
0280                 entryMap.setEntry(currentGroup, rawKey, lookup(line, &cache), entryOptions);
0281             } else {
0282                 entryMap.setEntry(currentGroup, lookup(aKey, &cache), lookup(line, &cache), entryOptions);
0283             }
0284         }
0285     next_line:
0286         continue;
0287     }
0288 
0289     // now make sure immutable groups are marked immutable
0290     for (const QString &group : std::as_const(immutableGroups)) {
0291         entryMap.setEntry(group, QByteArray(), QByteArray(), KEntryMap::EntryImmutable);
0292     }
0293 
0294     return fileOptionImmutable ? ParseImmutable : ParseOk;
0295 }
0296 
0297 void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map, bool defaultGroup, bool &firstEntry)
0298 {
0299     QString currentGroup;
0300     bool groupIsImmutable = false;
0301     for (const auto &[key, entry] : map) {
0302         // Either process the default group or all others
0303         if ((key.mGroup != QStringLiteral("<default>")) == defaultGroup) {
0304             continue; // skip
0305         }
0306 
0307         // the only thing we care about groups is, is it immutable?
0308         if (key.mKey.isNull()) {
0309             groupIsImmutable = entry.bImmutable;
0310             continue; // skip
0311         }
0312 
0313         const KEntry &currentEntry = entry;
0314         if (!defaultGroup && currentGroup != key.mGroup) {
0315             if (!firstEntry) {
0316                 file.putChar('\n');
0317             }
0318             currentGroup = key.mGroup;
0319             for (int start = 0, end;; start = end + 1) {
0320                 file.putChar('[');
0321                 end = currentGroup.indexOf(QLatin1Char('\x1d'), start);
0322                 if (end < 0) {
0323                     int cgl = currentGroup.length();
0324                     if (currentGroup.at(start) == QLatin1Char('$') && cgl - start <= 10) {
0325                         for (int i = start + 1; i < cgl; i++) {
0326                             const QChar c = currentGroup.at(i);
0327                             if (c < QLatin1Char('a') || c > QLatin1Char('z')) {
0328                                 goto nope;
0329                             }
0330                         }
0331                         file.write("\\x24");
0332                         ++start;
0333                     }
0334                 nope:
0335                     // TODO: make stringToPrintable also process QString, to save the conversion here and below
0336                     file.write(stringToPrintable(QStringView(currentGroup).mid(start).toUtf8(), GroupString));
0337                     file.putChar(']');
0338                     if (groupIsImmutable) {
0339                         file.write("[$i]", 4);
0340                     }
0341                     file.putChar('\n');
0342                     break;
0343                 } else {
0344                     file.write(stringToPrintable(QStringView(currentGroup).mid(start, end - start).toUtf8(), GroupString));
0345                     file.putChar(']');
0346                 }
0347             }
0348         }
0349 
0350         firstEntry = false;
0351         // it is data for a group
0352 
0353         if (key.bRaw) { // unprocessed key with attached locale from merge
0354             file.write(key.mKey);
0355         } else {
0356             file.write(stringToPrintable(key.mKey, KeyString)); // Key
0357             if (key.bLocal && locale != "C") { // 'C' locale == untranslated
0358                 file.putChar('[');
0359                 file.write(locale); // locale tag
0360                 file.putChar(']');
0361             }
0362         }
0363         if (currentEntry.bDeleted) {
0364             if (currentEntry.bImmutable) {
0365                 file.write("[$di]", 5); // Deleted + immutable
0366             } else {
0367                 file.write("[$d]", 4); // Deleted
0368             }
0369         } else {
0370             if (currentEntry.bImmutable || currentEntry.bExpand) {
0371                 file.write("[$", 2);
0372                 if (currentEntry.bImmutable) {
0373                     file.putChar('i');
0374                 }
0375                 if (currentEntry.bExpand) {
0376                     file.putChar('e');
0377                 }
0378                 file.putChar(']');
0379             }
0380             file.putChar('=');
0381             file.write(stringToPrintable(currentEntry.mValue, ValueString));
0382         }
0383         file.putChar('\n');
0384     }
0385 }
0386 
0387 void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map)
0388 {
0389     bool firstEntry = true;
0390 
0391     // write default group
0392     writeEntries(locale, file, map, true, firstEntry);
0393 
0394     // write all other groups
0395     writeEntries(locale, file, map, false, firstEntry);
0396 }
0397 
0398 bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options)
0399 {
0400     Q_ASSERT(!filePath().isEmpty());
0401 
0402     KEntryMap writeMap;
0403     const bool bGlobal = options & WriteGlobal;
0404 
0405     // First, reparse the file on disk, to merge our changes with the ones done by other apps
0406     // Store the result into writeMap.
0407     {
0408         ParseOptions opts = ParseExpansions;
0409         if (bGlobal) {
0410             opts |= ParseGlobal;
0411         }
0412         ParseInfo info = parseConfig(locale, writeMap, opts, true);
0413         if (info != ParseOk) { // either there was an error or the file became immutable
0414             return false;
0415         }
0416     }
0417 
0418     for (auto &[key, entry] : entryMap) {
0419         if (!key.mKey.isEmpty() && !entry.bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too.
0420             continue;
0421         }
0422 
0423         // only write entries that have the same "globality" as the file
0424         if (entry.bGlobal == bGlobal) {
0425             if (entry.bReverted && entry.bOverridesGlobal) {
0426                 entry.bDeleted = true;
0427                 writeMap[key] = entry;
0428             } else if (entry.bReverted) {
0429                 writeMap.erase(key);
0430             } else if (!entry.bDeleted) {
0431                 writeMap[key] = entry;
0432             } else {
0433                 KEntryKey defaultKey = key;
0434                 defaultKey.bDefault = true;
0435                 if (entryMap.find(defaultKey) == entryMap.end() && !entry.bOverridesGlobal) {
0436                     writeMap.erase(key); // remove the deleted entry if there is no default
0437                     // qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal;
0438                 } else {
0439                     writeMap[key] = entry; // otherwise write an explicitly deleted entry
0440                     // qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal;
0441                 }
0442             }
0443             entry.bDirty = false;
0444         }
0445     }
0446 
0447     // now writeMap should contain only entries to be written
0448     // so write it out to disk
0449 
0450     // check if file exists
0451     QFile::Permissions fileMode = QFile::ReadUser | QFile::WriteUser;
0452     bool createNew = true;
0453 
0454     QFileInfo fi(filePath());
0455     if (fi.exists()) {
0456 #ifdef Q_OS_WIN
0457         // TODO: getuid does not exist on windows, use GetSecurityInfo and GetTokenInformation instead
0458         createNew = false;
0459 #else
0460         if (fi.ownerId() == ::getuid()) {
0461             // Preserve file mode if file exists and is owned by user.
0462             fileMode = fi.permissions();
0463         } else {
0464             // File is not owned by user:
0465             // Don't create new file but write to existing file instead.
0466             createNew = false;
0467         }
0468 #endif
0469     }
0470 
0471     if (createNew) {
0472         QSaveFile file(filePath());
0473         if (!file.open(QIODevice::WriteOnly)) {
0474 #ifdef Q_OS_ANDROID
0475             // HACK: when we are dealing with content:// URIs, QSaveFile has to rely on DirectWrite.
0476             // Otherwise this method returns a false and we're done.
0477             file.setDirectWriteFallback(true);
0478             if (!file.open(QIODevice::WriteOnly)) {
0479                 qWarning(KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString();
0480                 return false;
0481             }
0482 #else
0483             qWarning(KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString();
0484             return false;
0485 #endif
0486         }
0487 
0488         file.setTextModeEnabled(true); // to get eol translation
0489         writeEntries(locale, file, writeMap);
0490 
0491         if (!file.size() && (fileMode == (QFile::ReadUser | QFile::WriteUser))) {
0492             // File is empty and doesn't have special permissions: delete it.
0493             file.cancelWriting();
0494 
0495             if (fi.exists()) {
0496                 // also remove the old file in case it existed. this can happen
0497                 // when we delete all the entries in an existing config file.
0498                 // if we don't do this, then deletions and revertToDefault's
0499                 // will mysteriously fail
0500                 QFile::remove(filePath());
0501             }
0502         } else {
0503             // Normal case: Close the file
0504             if (file.commit()) {
0505                 QFile::setPermissions(filePath(), fileMode);
0506                 return true;
0507             }
0508             // Couldn't write. Disk full?
0509             qCWarning(KCONFIG_CORE_LOG) << "Couldn't write" << filePath() << ". Disk full?";
0510             return false;
0511         }
0512     } else {
0513         // Open existing file. *DON'T* create it if it suddenly does not exist!
0514 #if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
0515         int fd = QT_OPEN(QFile::encodeName(filePath()).constData(), O_WRONLY | O_TRUNC);
0516         if (fd < 0) {
0517             return false;
0518         }
0519         QFile f;
0520         if (!f.open(fd, QIODevice::WriteOnly)) {
0521             QT_CLOSE(fd);
0522             return false;
0523         }
0524         writeEntries(locale, f, writeMap);
0525         f.close();
0526         QT_CLOSE(fd);
0527 #else
0528         QFile f(filePath());
0529         // XXX This is broken - it DOES create the file if it is suddenly gone.
0530         if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
0531             return false;
0532         }
0533         f.setTextModeEnabled(true);
0534         writeEntries(locale, f, writeMap);
0535 #endif
0536     }
0537     return true;
0538 }
0539 
0540 bool KConfigIniBackend::isWritable() const
0541 {
0542     const QString filePath = this->filePath();
0543     if (filePath.isEmpty()) {
0544         return false;
0545     }
0546 
0547     QFileInfo file(filePath);
0548     if (file.exists()) {
0549         return file.isWritable();
0550     }
0551 
0552     // If the file does not exist, check if the deepest existing dir is writable
0553     QFileInfo dir(file.absolutePath());
0554     while (!dir.exists()) {
0555         QString parent = dir.absolutePath(); // Go up. Can't use cdUp() on non-existing dirs.
0556         if (parent == dir.filePath()) {
0557             // no parent
0558             return false;
0559         }
0560         dir.setFile(parent);
0561     }
0562     return dir.isDir() && dir.isWritable();
0563 }
0564 
0565 QString KConfigIniBackend::nonWritableErrorMessage() const
0566 {
0567     return tr("Configuration file \"%1\" not writable.\n").arg(filePath());
0568 }
0569 
0570 void KConfigIniBackend::createEnclosing()
0571 {
0572     const QString file = filePath();
0573     if (file.isEmpty()) {
0574         return; // nothing to do
0575     }
0576 
0577     // Create the containing dir, maybe it wasn't there
0578     QDir().mkpath(QFileInfo(file).absolutePath());
0579 }
0580 
0581 void KConfigIniBackend::setFilePath(const QString &path)
0582 {
0583     if (path.isEmpty()) {
0584         return;
0585     }
0586 
0587     Q_ASSERT(QDir::isAbsolutePath(path));
0588 
0589     const QFileInfo info(path);
0590     if (info.exists()) {
0591         setLocalFilePath(info.canonicalFilePath());
0592         return;
0593     }
0594 
0595     if (QString filePath = info.dir().canonicalPath(); !filePath.isEmpty()) {
0596         filePath += QLatin1Char('/') + info.fileName();
0597         setLocalFilePath(filePath);
0598     } else {
0599         setLocalFilePath(path);
0600     }
0601 }
0602 
0603 KConfigBase::AccessMode KConfigIniBackend::accessMode() const
0604 {
0605     if (filePath().isEmpty()) {
0606         return KConfigBase::NoAccess;
0607     }
0608 
0609     if (isWritable()) {
0610         return KConfigBase::ReadWrite;
0611     }
0612 
0613     return KConfigBase::ReadOnly;
0614 }
0615 
0616 bool KConfigIniBackend::lock()
0617 {
0618     Q_ASSERT(!filePath().isEmpty());
0619 
0620     m_mutex.lock();
0621 #ifdef Q_OS_ANDROID
0622     if (!lockFile) {
0623         // handle content Uris properly
0624         if (filePath().startsWith(QLatin1String("content://"))) {
0625             // we can't create file at an arbitrary location, so use internal storage to create one
0626 
0627             // NOTE: filename can be the same, but because this lock is short lived we may never have a collision
0628             lockFile = new QLockFile(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/")
0629                                      + QFileInfo(filePath()).fileName() + QLatin1String(".lock"));
0630         } else {
0631             lockFile = new QLockFile(filePath() + QLatin1String(".lock"));
0632         }
0633     }
0634 #else
0635     if (!lockFile) {
0636         lockFile = new QLockFile(filePath() + QLatin1String(".lock"));
0637     }
0638 #endif
0639 
0640     if (!lockFile->lock()) {
0641         m_mutex.unlock();
0642     }
0643 
0644     return lockFile->isLocked();
0645 }
0646 
0647 void KConfigIniBackend::unlock()
0648 {
0649     lockFile->unlock();
0650     delete lockFile;
0651     lockFile = nullptr;
0652     m_mutex.unlock();
0653 }
0654 
0655 bool KConfigIniBackend::isLocked() const
0656 {
0657     return lockFile && lockFile->isLocked();
0658 }
0659 
0660 namespace
0661 {
0662 // serialize an escaped byte at the end of @param data
0663 // @param data should have room for 4 bytes
0664 char *escapeByte(char *data, unsigned char s)
0665 {
0666     static const char nibbleLookup[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
0667     *data++ = '\\';
0668     *data++ = 'x';
0669     *data++ = nibbleLookup[s >> 4];
0670     *data++ = nibbleLookup[s & 0x0f];
0671     return data;
0672 }
0673 
0674 // Struct that represents a multi-byte UTF-8 character.
0675 // This struct is used to keep track of bytes that seem to be valid
0676 // UTF-8.
0677 struct Utf8Char {
0678 public:
0679     unsigned char bytes[4];
0680     unsigned char count;
0681     unsigned char charLength;
0682 
0683     Utf8Char()
0684     {
0685         clear();
0686         charLength = 0;
0687     }
0688     void clear()
0689     {
0690         count = 0;
0691     }
0692     // Add a byte to the UTF8 character.
0693     // When an additional byte leads to an invalid character, return false.
0694     bool addByte(unsigned char b)
0695     {
0696         if (count == 0) {
0697             if (b > 0xc1 && (b & 0xe0) == 0xc0) {
0698                 charLength = 2;
0699             } else if ((b & 0xf0) == 0xe0) {
0700                 charLength = 3;
0701             } else if (b < 0xf5 && (b & 0xf8) == 0xf0) {
0702                 charLength = 4;
0703             } else {
0704                 return false;
0705             }
0706             bytes[0] = b;
0707             count = 1;
0708         } else if (count < 4 && (b & 0xc0) == 0x80) {
0709             if (count == 1) {
0710                 if (charLength == 3 && bytes[0] == 0xe0 && b < 0xa0) {
0711                     return false; // overlong 3 byte sequence
0712                 }
0713                 if (charLength == 4) {
0714                     if (bytes[0] == 0xf0 && b < 0x90) {
0715                         return false; // overlong 4 byte sequence
0716                     }
0717                     if (bytes[0] == 0xf4 && b > 0x8f) {
0718                         return false; // Unicode value larger than U+10FFFF
0719                     }
0720                 }
0721             }
0722             bytes[count++] = b;
0723         } else {
0724             return false;
0725         }
0726         return true;
0727     }
0728     // Return true if Utf8Char contains one valid character.
0729     bool isComplete() const
0730     {
0731         return count > 0 && count == charLength;
0732     }
0733     // Add the bytes in this UTF8 character in escaped form to data.
0734     char *escapeBytes(char *data)
0735     {
0736         for (unsigned char i = 0; i < count; ++i) {
0737             data = escapeByte(data, bytes[i]);
0738         }
0739         clear();
0740         return data;
0741     }
0742     // Add the bytes of the UTF8 character to a buffer.
0743     // Only call this if isComplete() returns true.
0744     char *writeUtf8(char *data)
0745     {
0746         for (unsigned char i = 0; i < count; ++i) {
0747             *data++ = bytes[i];
0748         }
0749         clear();
0750         return data;
0751     }
0752     // Write the bytes in the UTF8 character literally, or, if the
0753     // character is not complete, write the escaped bytes.
0754     // This is useful to handle the state that remains after handling
0755     // all bytes in a buffer.
0756     char *write(char *data)
0757     {
0758         if (isComplete()) {
0759             data = writeUtf8(data);
0760         } else {
0761             data = escapeBytes(data);
0762         }
0763         return data;
0764     }
0765 };
0766 }
0767 
0768 QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type)
0769 {
0770     const int len = aString.size();
0771     if (len == 0) {
0772         return aString;
0773     }
0774 
0775     QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of len*4
0776     result.resize(len * 4); // Maximum 4x as long as source string due to \x<ab> escape sequences
0777     const char *s = aString.constData();
0778     int i = 0;
0779     char *data = result.data();
0780     char *start = data;
0781 
0782     // Protect leading space
0783     if (s[0] == ' ' && type != GroupString) {
0784         *data++ = '\\';
0785         *data++ = 's';
0786         ++i;
0787     }
0788     Utf8Char utf8;
0789 
0790     for (; i < len; ++i) {
0791         switch (s[i]) {
0792         default:
0793             if (utf8.addByte(s[i])) {
0794                 break;
0795             } else {
0796                 data = utf8.escapeBytes(data);
0797             }
0798             // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here
0799             if (((unsigned char)s[i]) < 32) {
0800                 goto doEscape;
0801             }
0802             // GroupString and KeyString should be valid UTF-8, but ValueString
0803             // can be a bytearray with non-UTF-8 bytes that should be escaped.
0804             if (type == ValueString && ((unsigned char)s[i]) >= 127) {
0805                 goto doEscape;
0806             }
0807             *data++ = s[i];
0808             break;
0809         case '\n':
0810             *data++ = '\\';
0811             *data++ = 'n';
0812             break;
0813         case '\t':
0814             *data++ = '\\';
0815             *data++ = 't';
0816             break;
0817         case '\r':
0818             *data++ = '\\';
0819             *data++ = 'r';
0820             break;
0821         case '\\':
0822             *data++ = '\\';
0823             *data++ = '\\';
0824             break;
0825         case '=':
0826             if (type != KeyString) {
0827                 *data++ = s[i];
0828                 break;
0829             }
0830             goto doEscape;
0831         case '[':
0832         case ']':
0833             // Above chars are OK to put in *value* strings as plaintext
0834             if (type == ValueString) {
0835                 *data++ = s[i];
0836                 break;
0837             }
0838         doEscape:
0839             data = escapeByte(data, s[i]);
0840             break;
0841         }
0842         if (utf8.isComplete()) {
0843             data = utf8.writeUtf8(data);
0844         }
0845     }
0846     data = utf8.write(data);
0847     *data = 0;
0848     result.resize(data - start);
0849 
0850     // Protect trailing space
0851     if (result.endsWith(' ') && type != GroupString) {
0852         result.replace(result.length() - 1, 1, "\\s");
0853     }
0854 
0855     return result;
0856 }
0857 
0858 char KConfigIniBackend::charFromHex(const char *str, const QFile &file, int line)
0859 {
0860     unsigned char ret = 0;
0861     for (int i = 0; i < 2; i++) {
0862         ret <<= 4;
0863         quint8 c = quint8(str[i]);
0864 
0865         if (c >= '0' && c <= '9') {
0866             ret |= c - '0';
0867         } else if (c >= 'a' && c <= 'f') {
0868             ret |= c - 'a' + 0x0a;
0869         } else if (c >= 'A' && c <= 'F') {
0870             ret |= c - 'A' + 0x0a;
0871         } else {
0872             QByteArray e(str, 2);
0873             e.prepend("\\x");
0874             qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << "Invalid hex character " << c << " in \\x<nn>-type escape sequence \"" << e.constData()
0875                                         << "\".";
0876             return 'x';
0877         }
0878     }
0879     return char(ret);
0880 }
0881 
0882 void KConfigIniBackend::printableToString(BufferFragment *aString, const QFile &file, int line)
0883 {
0884     if (aString->isEmpty() || aString->indexOf('\\') == -1) {
0885         return;
0886     }
0887     aString->trim();
0888     int l = aString->length();
0889     char *r = aString->data();
0890     char *str = r;
0891 
0892     for (int i = 0; i < l; i++, r++) {
0893         if (str[i] != '\\') {
0894             *r = str[i];
0895         } else {
0896             // Probable escape sequence
0897             ++i;
0898             if (i >= l) { // Line ends after backslash - stop.
0899                 *r = '\\';
0900                 break;
0901             }
0902 
0903             switch (str[i]) {
0904             case 's':
0905                 *r = ' ';
0906                 break;
0907             case 't':
0908                 *r = '\t';
0909                 break;
0910             case 'n':
0911                 *r = '\n';
0912                 break;
0913             case 'r':
0914                 *r = '\r';
0915                 break;
0916             case '\\':
0917                 *r = '\\';
0918                 break;
0919             case ';':
0920                 // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string
0921                 *r = '\\';
0922                 ++r;
0923                 *r = ';';
0924                 break;
0925             case ',':
0926                 // not really an escape sequence, but allowed in .desktop files, don't strip '\,' from the string
0927                 *r = '\\';
0928                 ++r;
0929                 *r = ',';
0930                 break;
0931             case 'x':
0932                 if (i + 2 < l) {
0933                     *r = charFromHex(str + i + 1, file, line);
0934                     i += 2;
0935                 } else {
0936                     *r = 'x';
0937                     i = l - 1;
0938                 }
0939                 break;
0940             default:
0941                 *r = '\\';
0942                 qCWarning(KCONFIG_CORE_LOG).noquote() << warningProlog(file, line) << QStringLiteral("Invalid escape sequence: «\\%1»").arg(str[i]);
0943             }
0944         }
0945     }
0946     aString->truncate(r - aString->constData());
0947 }
0948 
0949 #include "moc_kconfigini_p.cpp"