File indexing completed on 2024-05-12 15:34:11
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<QByteArray> 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 QByteArray currentGroup("<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 = 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 QByteArray &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 QByteArray currentGroup; 0300 bool groupIsImmutable = false; 0301 const auto end = map.cend(); 0302 for (auto it = map.cbegin(); it != end; ++it) { 0303 const KEntryKey &key = it.key(); 0304 0305 // Either process the default group or all others 0306 if ((key.mGroup != "<default>") == defaultGroup) { 0307 continue; // skip 0308 } 0309 0310 // the only thing we care about groups is, is it immutable? 0311 if (key.mKey.isNull()) { 0312 groupIsImmutable = it->bImmutable; 0313 continue; // skip 0314 } 0315 0316 const KEntry ¤tEntry = *it; 0317 if (!defaultGroup && currentGroup != key.mGroup) { 0318 if (!firstEntry) { 0319 file.putChar('\n'); 0320 } 0321 currentGroup = key.mGroup; 0322 for (int start = 0, end;; start = end + 1) { 0323 file.putChar('['); 0324 end = currentGroup.indexOf('\x1d', start); 0325 if (end < 0) { 0326 int cgl = currentGroup.length(); 0327 if (currentGroup.at(start) == '$' && cgl - start <= 10) { 0328 for (int i = start + 1; i < cgl; i++) { 0329 char c = currentGroup.at(i); 0330 if (c < 'a' || c > 'z') { 0331 goto nope; 0332 } 0333 } 0334 file.write("\\x24"); 0335 ++start; 0336 } 0337 nope: 0338 file.write(stringToPrintable(currentGroup.mid(start), GroupString)); 0339 file.putChar(']'); 0340 if (groupIsImmutable) { 0341 file.write("[$i]", 4); 0342 } 0343 file.putChar('\n'); 0344 break; 0345 } else { 0346 file.write(stringToPrintable(currentGroup.mid(start, end - start), GroupString)); 0347 file.putChar(']'); 0348 } 0349 } 0350 } 0351 0352 firstEntry = false; 0353 // it is data for a group 0354 0355 if (key.bRaw) { // unprocessed key with attached locale from merge 0356 file.write(key.mKey); 0357 } else { 0358 file.write(stringToPrintable(key.mKey, KeyString)); // Key 0359 if (key.bLocal && locale != "C") { // 'C' locale == untranslated 0360 file.putChar('['); 0361 file.write(locale); // locale tag 0362 file.putChar(']'); 0363 } 0364 } 0365 if (currentEntry.bDeleted) { 0366 if (currentEntry.bImmutable) { 0367 file.write("[$di]", 5); // Deleted + immutable 0368 } else { 0369 file.write("[$d]", 4); // Deleted 0370 } 0371 } else { 0372 if (currentEntry.bImmutable || currentEntry.bExpand) { 0373 file.write("[$", 2); 0374 if (currentEntry.bImmutable) { 0375 file.putChar('i'); 0376 } 0377 if (currentEntry.bExpand) { 0378 file.putChar('e'); 0379 } 0380 file.putChar(']'); 0381 } 0382 file.putChar('='); 0383 file.write(stringToPrintable(currentEntry.mValue, ValueString)); 0384 } 0385 file.putChar('\n'); 0386 } 0387 } 0388 0389 void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map) 0390 { 0391 bool firstEntry = true; 0392 0393 // write default group 0394 writeEntries(locale, file, map, true, firstEntry); 0395 0396 // write all other groups 0397 writeEntries(locale, file, map, false, firstEntry); 0398 } 0399 0400 bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options) 0401 { 0402 Q_ASSERT(!filePath().isEmpty()); 0403 0404 KEntryMap writeMap; 0405 const bool bGlobal = options & WriteGlobal; 0406 0407 // First, reparse the file on disk, to merge our changes with the ones done by other apps 0408 // Store the result into writeMap. 0409 { 0410 ParseOptions opts = ParseExpansions; 0411 if (bGlobal) { 0412 opts |= ParseGlobal; 0413 } 0414 ParseInfo info = parseConfig(locale, writeMap, opts, true); 0415 if (info != ParseOk) { // either there was an error or the file became immutable 0416 return false; 0417 } 0418 } 0419 0420 const KEntryMapIterator end = entryMap.end(); 0421 for (KEntryMapIterator it = entryMap.begin(); it != end; ++it) { 0422 if (!it.key().mKey.isEmpty() && !it->bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too. 0423 continue; 0424 } 0425 0426 const KEntryKey &key = it.key(); 0427 0428 // only write entries that have the same "globality" as the file 0429 if (it->bGlobal == bGlobal) { 0430 if (it->bReverted && it->bOverridesGlobal) { 0431 it->bDeleted = true; 0432 writeMap[key] = *it; 0433 } else if (it->bReverted) { 0434 writeMap.remove(key); 0435 } else if (!it->bDeleted) { 0436 writeMap[key] = *it; 0437 } else { 0438 KEntryKey defaultKey = key; 0439 defaultKey.bDefault = true; 0440 if (!entryMap.contains(defaultKey) && !it->bOverridesGlobal) { 0441 writeMap.remove(key); // remove the deleted entry if there is no default 0442 // qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal; 0443 } else { 0444 writeMap[key] = *it; // otherwise write an explicitly deleted entry 0445 // qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal; 0446 } 0447 } 0448 it->bDirty = false; 0449 } 0450 } 0451 0452 // now writeMap should contain only entries to be written 0453 // so write it out to disk 0454 0455 // check if file exists 0456 QFile::Permissions fileMode = QFile::ReadUser | QFile::WriteUser; 0457 bool createNew = true; 0458 0459 QFileInfo fi(filePath()); 0460 if (fi.exists()) { 0461 #ifdef Q_OS_WIN 0462 // TODO: getuid does not exist on windows, use GetSecurityInfo and GetTokenInformation instead 0463 createNew = false; 0464 #else 0465 if (fi.ownerId() == ::getuid()) { 0466 // Preserve file mode if file exists and is owned by user. 0467 fileMode = fi.permissions(); 0468 } else { 0469 // File is not owned by user: 0470 // Don't create new file but write to existing file instead. 0471 createNew = false; 0472 } 0473 #endif 0474 } 0475 0476 if (createNew) { 0477 QSaveFile file(filePath()); 0478 if (!file.open(QIODevice::WriteOnly)) { 0479 #ifdef Q_OS_ANDROID 0480 // HACK: when we are dealing with content:// URIs, QSaveFile has to rely on DirectWrite. 0481 // Otherwise this method returns a false and we're done. 0482 file.setDirectWriteFallback(true); 0483 if (!file.open(QIODevice::WriteOnly)) { 0484 qWarning(KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString(); 0485 return false; 0486 } 0487 #else 0488 qWarning(KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString(); 0489 return false; 0490 #endif 0491 } 0492 0493 file.setTextModeEnabled(true); // to get eol translation 0494 writeEntries(locale, file, writeMap); 0495 0496 if (!file.size() && (fileMode == (QFile::ReadUser | QFile::WriteUser))) { 0497 // File is empty and doesn't have special permissions: delete it. 0498 file.cancelWriting(); 0499 0500 if (fi.exists()) { 0501 // also remove the old file in case it existed. this can happen 0502 // when we delete all the entries in an existing config file. 0503 // if we don't do this, then deletions and revertToDefault's 0504 // will mysteriously fail 0505 QFile::remove(filePath()); 0506 } 0507 } else { 0508 // Normal case: Close the file 0509 if (file.commit()) { 0510 QFile::setPermissions(filePath(), fileMode); 0511 return true; 0512 } 0513 // Couldn't write. Disk full? 0514 qCWarning(KCONFIG_CORE_LOG) << "Couldn't write" << filePath() << ". Disk full?"; 0515 return false; 0516 } 0517 } else { 0518 // Open existing file. *DON'T* create it if it suddenly does not exist! 0519 #if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID) 0520 int fd = QT_OPEN(QFile::encodeName(filePath()).constData(), O_WRONLY | O_TRUNC); 0521 if (fd < 0) { 0522 return false; 0523 } 0524 QFile f; 0525 if (!f.open(fd, QIODevice::WriteOnly)) { 0526 QT_CLOSE(fd); 0527 return false; 0528 } 0529 writeEntries(locale, f, writeMap); 0530 f.close(); 0531 QT_CLOSE(fd); 0532 #else 0533 QFile f(filePath()); 0534 // XXX This is broken - it DOES create the file if it is suddenly gone. 0535 if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { 0536 return false; 0537 } 0538 f.setTextModeEnabled(true); 0539 writeEntries(locale, f, writeMap); 0540 #endif 0541 } 0542 return true; 0543 } 0544 0545 bool KConfigIniBackend::isWritable() const 0546 { 0547 const QString filePath = this->filePath(); 0548 if (filePath.isEmpty()) { 0549 return false; 0550 } 0551 0552 QFileInfo file(filePath); 0553 if (file.exists()) { 0554 return file.isWritable(); 0555 } 0556 0557 // If the file does not exist, check if the deepest existing dir is writable 0558 QFileInfo dir(file.absolutePath()); 0559 while (!dir.exists()) { 0560 QString parent = dir.absolutePath(); // Go up. Can't use cdUp() on non-existing dirs. 0561 if (parent == dir.filePath()) { 0562 // no parent 0563 return false; 0564 } 0565 dir.setFile(parent); 0566 } 0567 return dir.isDir() && dir.isWritable(); 0568 } 0569 0570 QString KConfigIniBackend::nonWritableErrorMessage() const 0571 { 0572 return tr("Configuration file \"%1\" not writable.\n").arg(filePath()); 0573 } 0574 0575 void KConfigIniBackend::createEnclosing() 0576 { 0577 const QString file = filePath(); 0578 if (file.isEmpty()) { 0579 return; // nothing to do 0580 } 0581 0582 // Create the containing dir, maybe it wasn't there 0583 QDir().mkpath(QFileInfo(file).absolutePath()); 0584 } 0585 0586 void KConfigIniBackend::setFilePath(const QString &path) 0587 { 0588 if (path.isEmpty()) { 0589 return; 0590 } 0591 0592 Q_ASSERT(QDir::isAbsolutePath(path)); 0593 0594 const QFileInfo info(path); 0595 if (info.exists()) { 0596 setLocalFilePath(info.canonicalFilePath()); 0597 return; 0598 } 0599 0600 if (QString filePath = info.dir().canonicalPath(); !filePath.isEmpty()) { 0601 filePath += QLatin1Char('/') + info.fileName(); 0602 setLocalFilePath(filePath); 0603 } else { 0604 setLocalFilePath(path); 0605 } 0606 } 0607 0608 KConfigBase::AccessMode KConfigIniBackend::accessMode() const 0609 { 0610 if (filePath().isEmpty()) { 0611 return KConfigBase::NoAccess; 0612 } 0613 0614 if (isWritable()) { 0615 return KConfigBase::ReadWrite; 0616 } 0617 0618 return KConfigBase::ReadOnly; 0619 } 0620 0621 bool KConfigIniBackend::lock() 0622 { 0623 Q_ASSERT(!filePath().isEmpty()); 0624 0625 m_mutex.lock(); 0626 #ifdef Q_OS_ANDROID 0627 if (!lockFile) { 0628 // handle content Uris properly 0629 if (filePath().startsWith(QLatin1String("content://"))) { 0630 // we can't create file at an arbitrary location, so use internal storage to create one 0631 0632 // NOTE: filename can be the same, but because this lock is short lived we may never have a collision 0633 lockFile = new QLockFile(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/") 0634 + QFileInfo(filePath()).fileName() + QLatin1String(".lock")); 0635 } else { 0636 lockFile = new QLockFile(filePath() + QLatin1String(".lock")); 0637 } 0638 } 0639 #else 0640 if (!lockFile) { 0641 lockFile = new QLockFile(filePath() + QLatin1String(".lock")); 0642 } 0643 #endif 0644 0645 if (!lockFile->lock()) { 0646 m_mutex.unlock(); 0647 } 0648 0649 return lockFile->isLocked(); 0650 } 0651 0652 void KConfigIniBackend::unlock() 0653 { 0654 lockFile->unlock(); 0655 delete lockFile; 0656 lockFile = nullptr; 0657 m_mutex.unlock(); 0658 } 0659 0660 bool KConfigIniBackend::isLocked() const 0661 { 0662 return lockFile && lockFile->isLocked(); 0663 } 0664 0665 namespace 0666 { 0667 // serialize an escaped byte at the end of @param data 0668 // @param data should have room for 4 bytes 0669 char *escapeByte(char *data, unsigned char s) 0670 { 0671 static const char nibbleLookup[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 0672 *data++ = '\\'; 0673 *data++ = 'x'; 0674 *data++ = nibbleLookup[s >> 4]; 0675 *data++ = nibbleLookup[s & 0x0f]; 0676 return data; 0677 } 0678 0679 // Struct that represents a multi-byte UTF-8 character. 0680 // This struct is used to keep track of bytes that seem to be valid 0681 // UTF-8. 0682 struct Utf8Char { 0683 public: 0684 unsigned char bytes[4]; 0685 unsigned char count; 0686 unsigned char charLength; 0687 0688 Utf8Char() 0689 { 0690 clear(); 0691 charLength = 0; 0692 } 0693 void clear() 0694 { 0695 count = 0; 0696 } 0697 // Add a byte to the UTF8 character. 0698 // When an additional byte leads to an invalid character, return false. 0699 bool addByte(unsigned char b) 0700 { 0701 if (count == 0) { 0702 if (b > 0xc1 && (b & 0xe0) == 0xc0) { 0703 charLength = 2; 0704 } else if ((b & 0xf0) == 0xe0) { 0705 charLength = 3; 0706 } else if (b < 0xf5 && (b & 0xf8) == 0xf0) { 0707 charLength = 4; 0708 } else { 0709 return false; 0710 } 0711 bytes[0] = b; 0712 count = 1; 0713 } else if (count < 4 && (b & 0xc0) == 0x80) { 0714 if (count == 1) { 0715 if (charLength == 3 && bytes[0] == 0xe0 && b < 0xa0) { 0716 return false; // overlong 3 byte sequence 0717 } 0718 if (charLength == 4) { 0719 if (bytes[0] == 0xf0 && b < 0x90) { 0720 return false; // overlong 4 byte sequence 0721 } 0722 if (bytes[0] == 0xf4 && b > 0x8f) { 0723 return false; // Unicode value larger than U+10FFFF 0724 } 0725 } 0726 } 0727 bytes[count++] = b; 0728 } else { 0729 return false; 0730 } 0731 return true; 0732 } 0733 // Return true if Utf8Char contains one valid character. 0734 bool isComplete() 0735 { 0736 return count > 0 && count == charLength; 0737 } 0738 // Add the bytes in this UTF8 character in escaped form to data. 0739 char *escapeBytes(char *data) 0740 { 0741 for (unsigned char i = 0; i < count; ++i) { 0742 data = escapeByte(data, bytes[i]); 0743 } 0744 clear(); 0745 return data; 0746 } 0747 // Add the bytes of the UTF8 character to a buffer. 0748 // Only call this if isComplete() returns true. 0749 char *writeUtf8(char *data) 0750 { 0751 for (unsigned char i = 0; i < count; ++i) { 0752 *data++ = bytes[i]; 0753 } 0754 clear(); 0755 return data; 0756 } 0757 // Write the bytes in the UTF8 character literally, or, if the 0758 // character is not complete, write the escaped bytes. 0759 // This is useful to handle the state that remains after handling 0760 // all bytes in a buffer. 0761 char *write(char *data) 0762 { 0763 if (isComplete()) { 0764 data = writeUtf8(data); 0765 } else { 0766 data = escapeBytes(data); 0767 } 0768 return data; 0769 } 0770 }; 0771 } 0772 0773 QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type) 0774 { 0775 const int len = aString.size(); 0776 if (len == 0) { 0777 return aString; 0778 } 0779 0780 QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of len*4 0781 result.resize(len * 4); // Maximum 4x as long as source string due to \x<ab> escape sequences 0782 const char *s = aString.constData(); 0783 int i = 0; 0784 char *data = result.data(); 0785 char *start = data; 0786 0787 // Protect leading space 0788 if (s[0] == ' ' && type != GroupString) { 0789 *data++ = '\\'; 0790 *data++ = 's'; 0791 ++i; 0792 } 0793 Utf8Char utf8; 0794 0795 for (; i < len; ++i) { 0796 switch (s[i]) { 0797 default: 0798 if (utf8.addByte(s[i])) { 0799 break; 0800 } else { 0801 data = utf8.escapeBytes(data); 0802 } 0803 // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here 0804 if (((unsigned char)s[i]) < 32) { 0805 goto doEscape; 0806 } 0807 // GroupString and KeyString should be valid UTF-8, but ValueString 0808 // can be a bytearray with non-UTF-8 bytes that should be escaped. 0809 if (type == ValueString && ((unsigned char)s[i]) >= 127) { 0810 goto doEscape; 0811 } 0812 *data++ = s[i]; 0813 break; 0814 case '\n': 0815 *data++ = '\\'; 0816 *data++ = 'n'; 0817 break; 0818 case '\t': 0819 *data++ = '\\'; 0820 *data++ = 't'; 0821 break; 0822 case '\r': 0823 *data++ = '\\'; 0824 *data++ = 'r'; 0825 break; 0826 case '\\': 0827 *data++ = '\\'; 0828 *data++ = '\\'; 0829 break; 0830 case '=': 0831 if (type != KeyString) { 0832 *data++ = s[i]; 0833 break; 0834 } 0835 goto doEscape; 0836 case '[': 0837 case ']': 0838 // Above chars are OK to put in *value* strings as plaintext 0839 if (type == ValueString) { 0840 *data++ = s[i]; 0841 break; 0842 } 0843 doEscape: 0844 data = escapeByte(data, s[i]); 0845 break; 0846 } 0847 if (utf8.isComplete()) { 0848 data = utf8.writeUtf8(data); 0849 } 0850 } 0851 data = utf8.write(data); 0852 *data = 0; 0853 result.resize(data - start); 0854 0855 // Protect trailing space 0856 if (result.endsWith(' ') && type != GroupString) { 0857 result.replace(result.length() - 1, 1, "\\s"); 0858 } 0859 0860 return result; 0861 } 0862 0863 char KConfigIniBackend::charFromHex(const char *str, const QFile &file, int line) 0864 { 0865 unsigned char ret = 0; 0866 for (int i = 0; i < 2; i++) { 0867 ret <<= 4; 0868 quint8 c = quint8(str[i]); 0869 0870 if (c >= '0' && c <= '9') { 0871 ret |= c - '0'; 0872 } else if (c >= 'a' && c <= 'f') { 0873 ret |= c - 'a' + 0x0a; 0874 } else if (c >= 'A' && c <= 'F') { 0875 ret |= c - 'A' + 0x0a; 0876 } else { 0877 QByteArray e(str, 2); 0878 e.prepend("\\x"); 0879 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << "Invalid hex character " << c << " in \\x<nn>-type escape sequence \"" << e.constData() 0880 << "\"."; 0881 return 'x'; 0882 } 0883 } 0884 return char(ret); 0885 } 0886 0887 void KConfigIniBackend::printableToString(BufferFragment *aString, const QFile &file, int line) 0888 { 0889 if (aString->isEmpty() || aString->indexOf('\\') == -1) { 0890 return; 0891 } 0892 aString->trim(); 0893 int l = aString->length(); 0894 char *r = aString->data(); 0895 char *str = r; 0896 0897 for (int i = 0; i < l; i++, r++) { 0898 if (str[i] != '\\') { 0899 *r = str[i]; 0900 } else { 0901 // Probable escape sequence 0902 ++i; 0903 if (i >= l) { // Line ends after backslash - stop. 0904 *r = '\\'; 0905 break; 0906 } 0907 0908 switch (str[i]) { 0909 case 's': 0910 *r = ' '; 0911 break; 0912 case 't': 0913 *r = '\t'; 0914 break; 0915 case 'n': 0916 *r = '\n'; 0917 break; 0918 case 'r': 0919 *r = '\r'; 0920 break; 0921 case '\\': 0922 *r = '\\'; 0923 break; 0924 case ';': 0925 // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string 0926 *r = '\\'; 0927 ++r; 0928 *r = ';'; 0929 break; 0930 case ',': 0931 // not really an escape sequence, but allowed in .desktop files, don't strip '\,' from the string 0932 *r = '\\'; 0933 ++r; 0934 *r = ','; 0935 break; 0936 case 'x': 0937 if (i + 2 < l) { 0938 *r = charFromHex(str + i + 1, file, line); 0939 i += 2; 0940 } else { 0941 *r = 'x'; 0942 i = l - 1; 0943 } 0944 break; 0945 default: 0946 *r = '\\'; 0947 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << QStringLiteral("Invalid escape sequence \"\\%1\".").arg(str[i]); 0948 } 0949 } 0950 } 0951 aString->truncate(r - aString->constData()); 0952 } 0953 0954 #include "moc_kconfigini_p.cpp"