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 ¤tLocale, 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 ¤tLocale, 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 ¤tEntry = 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"