1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2006, 2007 Thomas Braxton <kde.braxton@gmail.com>
4 SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org>
5 SPDX-FileCopyrightText: 1997-1999 Matthias Kalle Dalheimer <kalle@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "kconfigini_p.h"
11
12#include "bufferfragment_p.h"
13#include "kconfig.h"
14#include "kconfig_core_log_settings.h"
15#include "kconfigbackend_p.h"
16#include "kconfigdata_p.h"
17
18#include <QDateTime>
19#include <QDebug>
20#include <QDir>
21#include <QFile>
22#include <QFileInfo>
23#include <QLockFile>
24#include <QSaveFile>
25#include <qplatformdefs.h>
26
27#ifndef Q_OS_WIN
28#include <unistd.h> // getuid, close
29#endif
30#include <fcntl.h> // open
31#include <sys/types.h> // uid_t
32
33KCONFIGCORE_EXPORT bool kde_kiosk_exception = false; // flag to disable kiosk restrictions
34
35static QByteArray lookup(const KConfigIniBackend::BufferFragment fragment, QHash<KConfigIniBackend::BufferFragment, QByteArray> *cache)
36{
37 auto it = cache->constFind(key: fragment);
38 if (it != cache->constEnd()) {
39 return it.value();
40 }
41
42 return cache->insert(key: fragment, value: fragment.toByteArray()).value();
43}
44
45QString KConfigIniBackend::warningProlog(const QFile &file, int line)
46{
47 // %2 then %1 i.e. int before QString, so that the QString is last
48 // This avoids a wrong substitution if the fileName itself contains %1
49 return QStringLiteral("KConfigIni: In file %2, line %1:").arg(a: line).arg(a: file.fileName());
50}
51
52KConfigIniBackend::KConfigIniBackend()
53 : KConfigBackend()
54 , lockFile(nullptr)
55{
56}
57
58KConfigIniBackend::~KConfigIniBackend()
59{
60}
61
62KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options)
63{
64 return parseConfig(locale: currentLocale, entryMap, options, merging: false);
65}
66
67// merging==true is the merging that happens at the beginning of writeConfig:
68// merge changes in the on-disk file with the changes in the KConfig object.
69KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options, bool merging)
70{
71 if (filePath().isEmpty()) {
72 return ParseOk;
73 }
74
75 QFile file(filePath());
76 if (!file.open(flags: QIODevice::ReadOnly | QIODevice::Text)) {
77 return file.exists() ? ParseOpenError : ParseOk;
78 }
79
80 QList<QString> immutableGroups;
81
82 bool fileOptionImmutable = false;
83 bool groupOptionImmutable = false;
84 bool groupSkip = false;
85
86 int lineNo = 0;
87 // on systems using \r\n as end of line, \r will be taken care of by
88 // trim() below
89 QByteArray buffer = file.readAll();
90 BufferFragment contents(buffer.data(), buffer.size());
91 unsigned int len = contents.length();
92 unsigned int startOfLine = 0;
93
94 const int langIdx = currentLocale.indexOf(c: '_');
95 const QByteArray currentLanguage = langIdx >= 0 ? currentLocale.left(len: langIdx) : currentLocale;
96
97 QString currentGroup = QStringLiteral("<default>");
98 bool bDefault = options & ParseDefaults;
99 bool allowExecutableValues = options & ParseExpansions;
100
101 // Reduce memory overhead by making use of implicit sharing
102 // This assumes that config files contain only a small amount of
103 // different fragments which are repeated often.
104 // This is often the case, especially sub groups will all have
105 // the same list of keys and similar values as well.
106 QHash<BufferFragment, QByteArray> cache;
107 cache.reserve(size: 4096);
108
109 while (startOfLine < len) {
110 BufferFragment line = contents.split(c: '\n', start: &startOfLine);
111 line.trim();
112 ++lineNo;
113
114 // skip empty lines and lines beginning with '#'
115 if (line.isEmpty() || line.at(i: 0) == '#') {
116 continue;
117 }
118
119 if (line.at(i: 0) == '[') { // found a group
120 groupOptionImmutable = fileOptionImmutable;
121
122 QByteArray newGroup;
123 int start = 1;
124 int end = 0;
125 do {
126 end = start;
127 for (;;) {
128 if (end == line.length()) {
129 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line: lineNo) << "Invalid group header.";
130 // XXX maybe reset the current group here?
131 goto next_line;
132 }
133 if (line.at(i: end) == ']') {
134 break;
135 }
136 ++end;
137 }
138 /* clang-format off */
139 if (end + 1 == line.length()
140 && start + 2 == end
141 && line.at(i: start) == '$'
142 && line.at(i: start + 1) == 'i') { /* clang-format on */
143 if (newGroup.isEmpty()) {
144 fileOptionImmutable = !kde_kiosk_exception;
145 } else {
146 groupOptionImmutable = !kde_kiosk_exception;
147 }
148 } else {
149 if (!newGroup.isEmpty()) {
150 newGroup += '\x1d';
151 }
152 BufferFragment namePart = line.mid(pos: start, length: end - start);
153 printableToString(aString: &namePart, file, line: lineNo);
154 newGroup += namePart.toByteArray();
155 }
156 } while ((start = end + 2) <= line.length() && line.at(i: end + 1) == '[');
157 currentGroup = QString::fromUtf8(ba: newGroup);
158
159 groupSkip = entryMap.getEntryOption(group: currentGroup, key: {}, flags: {}, option: KEntryMap::EntryImmutable);
160
161 if (groupSkip && !bDefault) {
162 continue;
163 }
164
165 if (groupOptionImmutable)
166 // Do not make the groups immutable until the entries from
167 // this file have been added.
168 {
169 immutableGroups.append(t: currentGroup);
170 }
171 } else {
172 if (groupSkip && !bDefault) {
173 continue; // skip entry
174 }
175
176 BufferFragment aKey;
177 int eqpos = line.indexOf(c: '=');
178 if (eqpos < 0) {
179 aKey = line;
180 line.clear();
181 } else {
182 BufferFragment temp = line.left(size: eqpos);
183 temp.trim();
184 aKey = temp;
185 line.truncateLeft(size: eqpos + 1);
186 line.trim();
187 }
188 if (aKey.isEmpty()) {
189 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line: lineNo) << "Invalid entry (empty key)";
190 continue;
191 }
192
193 KEntryMap::EntryOptions entryOptions = {};
194 if (groupOptionImmutable) {
195 entryOptions |= KEntryMap::EntryImmutable;
196 }
197
198 BufferFragment locale;
199 int start;
200 while ((start = aKey.lastIndexOf(c: '[')) >= 0) {
201 int end = aKey.indexOf(c: ']', from: start);
202 if (end < 0) {
203 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line: lineNo) << "Invalid entry (missing ']')";
204 goto next_line;
205 } else if (end > start + 1 && aKey.at(i: start + 1) == '$') { // found option(s)
206 int i = start + 2;
207 while (i < end) {
208 switch (aKey.at(i)) {
209 case 'i':
210 if (!kde_kiosk_exception) {
211 entryOptions |= KEntryMap::EntryImmutable;
212 }
213 break;
214 case 'e':
215 if (allowExecutableValues) {
216 entryOptions |= KEntryMap::EntryExpansion;
217 }
218 break;
219 case 'd':
220 entryOptions |= KEntryMap::EntryDeleted;
221 aKey.truncate(pos: start);
222 printableToString(aString: &aKey, file, line: lineNo);
223 entryMap.setEntry(group: currentGroup, key: aKey.toByteArray(), value: QByteArray(), options: entryOptions);
224 goto next_line;
225 default:
226 break;
227 }
228 ++i;
229 }
230 } else { // found a locale
231 if (!locale.isNull()) {
232 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line: lineNo) << "Invalid entry (second locale!?)";
233 goto next_line;
234 }
235
236 locale = aKey.mid(pos: start + 1, length: end - start - 1);
237 }
238 aKey.truncate(pos: start);
239 }
240 if (eqpos < 0) { // Do this here after [$d] was checked
241 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line: lineNo) << "Invalid entry (missing '=')";
242 continue;
243 }
244 printableToString(aString: &aKey, file, line: lineNo);
245 if (!locale.isEmpty()) {
246 if (locale != currentLocale && locale != currentLanguage) {
247 // backward compatibility. C == en_US
248 if (locale.at(i: 0) != 'C' || currentLocale != "en_US") {
249 if (merging) {
250 entryOptions |= KEntryMap::EntryRawKey;
251 } else {
252 goto next_line; // skip this entry if we're not merging
253 }
254 }
255 }
256 }
257
258 if (!(entryOptions & KEntryMap::EntryRawKey)) {
259 printableToString(aString: &aKey, file, line: lineNo);
260 }
261
262 if (options & ParseGlobal) {
263 entryOptions |= KEntryMap::EntryGlobal;
264 }
265 if (bDefault) {
266 entryOptions |= KEntryMap::EntryDefault;
267 }
268 if (!locale.isNull()) {
269 entryOptions |= KEntryMap::EntryLocalized;
270 if (locale.indexOf(c: '_') != -1) {
271 entryOptions |= KEntryMap::EntryLocalizedCountry;
272 }
273 }
274 printableToString(aString: &line, file, line: lineNo);
275 if (entryOptions & KEntryMap::EntryRawKey) {
276 QByteArray rawKey;
277 rawKey.reserve(asize: aKey.length() + locale.length() + 2);
278 rawKey.append(a: aKey.toVolatileByteArray());
279 rawKey.append(c: '[').append(a: locale.toVolatileByteArray()).append(c: ']');
280 entryMap.setEntry(group: currentGroup, key: rawKey, value: lookup(fragment: line, cache: &cache), options: entryOptions);
281 } else {
282 entryMap.setEntry(group: currentGroup, key: lookup(fragment: aKey, cache: &cache), value: lookup(fragment: line, cache: &cache), options: entryOptions);
283 }
284 }
285 next_line:
286 continue;
287 }
288
289 // now make sure immutable groups are marked immutable
290 for (const QString &group : std::as_const(t&: immutableGroups)) {
291 entryMap.setEntry(group, key: QByteArray(), value: QByteArray(), options: KEntryMap::EntryImmutable);
292 }
293
294 return fileOptionImmutable ? ParseImmutable : ParseOk;
295}
296
297void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map, bool defaultGroup, bool &firstEntry)
298{
299 QString currentGroup;
300 bool groupIsImmutable = false;
301 for (const auto &[key, entry] : map) {
302 // Either process the default group or all others
303 if ((key.mGroup != QStringLiteral("<default>")) == defaultGroup) {
304 continue; // skip
305 }
306
307 // the only thing we care about groups is, is it immutable?
308 if (key.mKey.isNull()) {
309 groupIsImmutable = entry.bImmutable;
310 continue; // skip
311 }
312
313 const KEntry &currentEntry = entry;
314 if (!defaultGroup && currentGroup != key.mGroup) {
315 if (!firstEntry) {
316 file.putChar(c: '\n');
317 }
318 currentGroup = key.mGroup;
319 for (int start = 0, end;; start = end + 1) {
320 file.putChar(c: '[');
321 end = currentGroup.indexOf(c: QLatin1Char('\x1d'), from: start);
322 if (end < 0) {
323 int cgl = currentGroup.length();
324 if (currentGroup.at(i: start) == QLatin1Char('$') && cgl - start <= 10) {
325 for (int i = start + 1; i < cgl; i++) {
326 const QChar c = currentGroup.at(i);
327 if (c < QLatin1Char('a') || c > QLatin1Char('z')) {
328 goto nope;
329 }
330 }
331 file.write(data: "\\x24");
332 ++start;
333 }
334 nope:
335 // TODO: make stringToPrintable also process QString, to save the conversion here and below
336 file.write(data: stringToPrintable(aString: QStringView(currentGroup).mid(pos: start).toUtf8(), type: GroupString));
337 file.putChar(c: ']');
338 if (groupIsImmutable) {
339 file.write(data: "[$i]", len: 4);
340 }
341 file.putChar(c: '\n');
342 break;
343 } else {
344 file.write(data: stringToPrintable(aString: QStringView(currentGroup).mid(pos: start, n: end - start).toUtf8(), type: GroupString));
345 file.putChar(c: ']');
346 }
347 }
348 }
349
350 firstEntry = false;
351 // it is data for a group
352
353 if (key.bRaw) { // unprocessed key with attached locale from merge
354 file.write(data: key.mKey);
355 } else {
356 file.write(data: stringToPrintable(aString: key.mKey, type: KeyString)); // Key
357 if (key.bLocal && locale != "C") { // 'C' locale == untranslated
358 file.putChar(c: '[');
359 file.write(data: locale); // locale tag
360 file.putChar(c: ']');
361 }
362 }
363 if (currentEntry.bDeleted) {
364 if (currentEntry.bImmutable) {
365 file.write(data: "[$di]", len: 5); // Deleted + immutable
366 } else {
367 file.write(data: "[$d]", len: 4); // Deleted
368 }
369 } else {
370 if (currentEntry.bImmutable || currentEntry.bExpand) {
371 file.write(data: "[$", len: 2);
372 if (currentEntry.bImmutable) {
373 file.putChar(c: 'i');
374 }
375 if (currentEntry.bExpand) {
376 file.putChar(c: 'e');
377 }
378 file.putChar(c: ']');
379 }
380 file.putChar(c: '=');
381 file.write(data: stringToPrintable(aString: currentEntry.mValue, type: ValueString));
382 }
383 file.putChar(c: '\n');
384 }
385}
386
387void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map)
388{
389 bool firstEntry = true;
390
391 // write default group
392 writeEntries(locale, file, map, defaultGroup: true, firstEntry);
393
394 // write all other groups
395 writeEntries(locale, file, map, defaultGroup: false, firstEntry);
396}
397
398bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options)
399{
400 Q_ASSERT(!filePath().isEmpty());
401
402 KEntryMap writeMap;
403 const bool bGlobal = options & WriteGlobal;
404
405 // First, reparse the file on disk, to merge our changes with the ones done by other apps
406 // Store the result into writeMap.
407 {
408 ParseOptions opts = ParseExpansions;
409 if (bGlobal) {
410 opts |= ParseGlobal;
411 }
412 ParseInfo info = parseConfig(currentLocale: locale, entryMap&: writeMap, options: opts, merging: true);
413 if (info != ParseOk) { // either there was an error or the file became immutable
414 return false;
415 }
416 }
417
418 for (auto &[key, entry] : entryMap) {
419 if (!key.mKey.isEmpty() && !entry.bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too.
420 continue;
421 }
422
423 // only write entries that have the same "globality" as the file
424 if (entry.bGlobal == bGlobal) {
425 if (entry.bReverted && entry.bOverridesGlobal) {
426 entry.bDeleted = true;
427 writeMap[key] = entry;
428 } else if (entry.bReverted) {
429 writeMap.erase(x: key);
430 } else if (!entry.bDeleted) {
431 writeMap[key] = entry;
432 } else {
433 KEntryKey defaultKey = key;
434 defaultKey.bDefault = true;
435 if (entryMap.find(x: defaultKey) == entryMap.end() && !entry.bOverridesGlobal) {
436 writeMap.erase(x: key); // remove the deleted entry if there is no default
437 // qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal;
438 } else {
439 writeMap[key] = entry; // otherwise write an explicitly deleted entry
440 // qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal;
441 }
442 }
443 entry.bDirty = false;
444 }
445 }
446
447 // now writeMap should contain only entries to be written
448 // so write it out to disk
449
450 // check if file exists
451 QFile::Permissions fileMode = QFile::ReadUser | QFile::WriteUser;
452 bool createNew = true;
453
454 QFileInfo fi(filePath());
455 if (fi.exists()) {
456#ifdef Q_OS_WIN
457 // TODO: getuid does not exist on windows, use GetSecurityInfo and GetTokenInformation instead
458 createNew = false;
459#else
460 if (fi.ownerId() == ::getuid()) {
461 // Preserve file mode if file exists and is owned by user.
462 fileMode = fi.permissions();
463 } else {
464 // File is not owned by user:
465 // Don't create new file but write to existing file instead.
466 createNew = false;
467 }
468#endif
469 }
470
471 if (createNew) {
472 QSaveFile file(filePath());
473 if (!file.open(flags: QIODevice::WriteOnly)) {
474#ifdef Q_OS_ANDROID
475 // HACK: when we are dealing with content:// URIs, QSaveFile has to rely on DirectWrite.
476 // Otherwise this method returns a false and we're done.
477 file.setDirectWriteFallback(true);
478 if (!file.open(QIODevice::WriteOnly)) {
479 qWarning(KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString();
480 return false;
481 }
482#else
483 qWarning(catFunc: KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString();
484 return false;
485#endif
486 }
487
488 file.setTextModeEnabled(true); // to get eol translation
489 writeEntries(locale, file, map: writeMap);
490
491 if (!file.size() && (fileMode == (QFile::ReadUser | QFile::WriteUser))) {
492 // File is empty and doesn't have special permissions: delete it.
493 file.cancelWriting();
494
495 if (fi.exists()) {
496 // also remove the old file in case it existed. this can happen
497 // when we delete all the entries in an existing config file.
498 // if we don't do this, then deletions and revertToDefault's
499 // will mysteriously fail
500 QFile::remove(fileName: filePath());
501 }
502 } else {
503 // Normal case: Close the file
504 if (file.commit()) {
505 QFile::setPermissions(filename: filePath(), permissionSpec: fileMode);
506 return true;
507 }
508 // Couldn't write. Disk full?
509 qCWarning(KCONFIG_CORE_LOG) << "Couldn't write" << filePath() << ". Disk full?";
510 return false;
511 }
512 } else {
513 // Open existing file. *DON'T* create it if it suddenly does not exist!
514#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
515 int fd = QT_OPEN(file: QFile::encodeName(fileName: filePath()).constData(), O_WRONLY | O_TRUNC);
516 if (fd < 0) {
517 return false;
518 }
519 QFile f;
520 if (!f.open(fd, ioFlags: QIODevice::WriteOnly)) {
521 QT_CLOSE(fd: fd);
522 return false;
523 }
524 writeEntries(locale, file&: f, map: writeMap);
525 f.close();
526 QT_CLOSE(fd: fd);
527#else
528 QFile f(filePath());
529 // XXX This is broken - it DOES create the file if it is suddenly gone.
530 if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
531 return false;
532 }
533 f.setTextModeEnabled(true);
534 writeEntries(locale, f, writeMap);
535#endif
536 }
537 return true;
538}
539
540bool KConfigIniBackend::isWritable() const
541{
542 const QString filePath = this->filePath();
543 if (filePath.isEmpty()) {
544 return false;
545 }
546
547 QFileInfo file(filePath);
548 if (file.exists()) {
549 return file.isWritable();
550 }
551
552 // If the file does not exist, check if the deepest existing dir is writable
553 QFileInfo dir(file.absolutePath());
554 while (!dir.exists()) {
555 QString parent = dir.absolutePath(); // Go up. Can't use cdUp() on non-existing dirs.
556 if (parent == dir.filePath()) {
557 // no parent
558 return false;
559 }
560 dir.setFile(parent);
561 }
562 return dir.isDir() && dir.isWritable();
563}
564
565QString KConfigIniBackend::nonWritableErrorMessage() const
566{
567 return tr(s: "Configuration file \"%1\" not writable.\n").arg(a: filePath());
568}
569
570void KConfigIniBackend::createEnclosing()
571{
572 const QString file = filePath();
573 if (file.isEmpty()) {
574 return; // nothing to do
575 }
576
577 // Create the containing dir, maybe it wasn't there
578 QDir().mkpath(dirPath: QFileInfo(file).absolutePath());
579}
580
581void KConfigIniBackend::setFilePath(const QString &path)
582{
583 if (path.isEmpty()) {
584 return;
585 }
586
587 Q_ASSERT(QDir::isAbsolutePath(path));
588
589 const QFileInfo info(path);
590 if (info.exists()) {
591 setLocalFilePath(info.canonicalFilePath());
592 return;
593 }
594
595 if (QString filePath = info.dir().canonicalPath(); !filePath.isEmpty()) {
596 filePath += QLatin1Char('/') + info.fileName();
597 setLocalFilePath(filePath);
598 } else {
599 setLocalFilePath(path);
600 }
601}
602
603KConfigBase::AccessMode KConfigIniBackend::accessMode() const
604{
605 if (filePath().isEmpty()) {
606 return KConfigBase::NoAccess;
607 }
608
609 if (isWritable()) {
610 return KConfigBase::ReadWrite;
611 }
612
613 return KConfigBase::ReadOnly;
614}
615
616bool KConfigIniBackend::lock()
617{
618 Q_ASSERT(!filePath().isEmpty());
619
620 m_mutex.lock();
621#ifdef Q_OS_ANDROID
622 if (!lockFile) {
623 // handle content Uris properly
624 if (filePath().startsWith(QLatin1String("content://"))) {
625 // we can't create file at an arbitrary location, so use internal storage to create one
626
627 // NOTE: filename can be the same, but because this lock is short lived we may never have a collision
628 lockFile = new QLockFile(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/")
629 + QFileInfo(filePath()).fileName() + QLatin1String(".lock"));
630 } else {
631 lockFile = new QLockFile(filePath() + QLatin1String(".lock"));
632 }
633 }
634#else
635 if (!lockFile) {
636 lockFile = new QLockFile(filePath() + QLatin1String(".lock"));
637 }
638#endif
639
640 if (!lockFile->lock()) {
641 m_mutex.unlock();
642 }
643
644 return lockFile->isLocked();
645}
646
647void KConfigIniBackend::unlock()
648{
649 lockFile->unlock();
650 delete lockFile;
651 lockFile = nullptr;
652 m_mutex.unlock();
653}
654
655bool KConfigIniBackend::isLocked() const
656{
657 return lockFile && lockFile->isLocked();
658}
659
660namespace
661{
662// serialize an escaped byte at the end of @param data
663// @param data should have room for 4 bytes
664char *escapeByte(char *data, unsigned char s)
665{
666 static const char nibbleLookup[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
667 *data++ = '\\';
668 *data++ = 'x';
669 *data++ = nibbleLookup[s >> 4];
670 *data++ = nibbleLookup[s & 0x0f];
671 return data;
672}
673
674// Struct that represents a multi-byte UTF-8 character.
675// This struct is used to keep track of bytes that seem to be valid
676// UTF-8.
677struct Utf8Char {
678public:
679 unsigned char bytes[4];
680 unsigned char count;
681 unsigned char charLength;
682
683 Utf8Char()
684 {
685 clear();
686 charLength = 0;
687 }
688 void clear()
689 {
690 count = 0;
691 }
692 // Add a byte to the UTF8 character.
693 // When an additional byte leads to an invalid character, return false.
694 bool addByte(unsigned char b)
695 {
696 if (count == 0) {
697 if (b > 0xc1 && (b & 0xe0) == 0xc0) {
698 charLength = 2;
699 } else if ((b & 0xf0) == 0xe0) {
700 charLength = 3;
701 } else if (b < 0xf5 && (b & 0xf8) == 0xf0) {
702 charLength = 4;
703 } else {
704 return false;
705 }
706 bytes[0] = b;
707 count = 1;
708 } else if (count < 4 && (b & 0xc0) == 0x80) {
709 if (count == 1) {
710 if (charLength == 3 && bytes[0] == 0xe0 && b < 0xa0) {
711 return false; // overlong 3 byte sequence
712 }
713 if (charLength == 4) {
714 if (bytes[0] == 0xf0 && b < 0x90) {
715 return false; // overlong 4 byte sequence
716 }
717 if (bytes[0] == 0xf4 && b > 0x8f) {
718 return false; // Unicode value larger than U+10FFFF
719 }
720 }
721 }
722 bytes[count++] = b;
723 } else {
724 return false;
725 }
726 return true;
727 }
728 // Return true if Utf8Char contains one valid character.
729 bool isComplete() const
730 {
731 return count > 0 && count == charLength;
732 }
733 // Add the bytes in this UTF8 character in escaped form to data.
734 char *escapeBytes(char *data)
735 {
736 for (unsigned char i = 0; i < count; ++i) {
737 data = escapeByte(data, s: bytes[i]);
738 }
739 clear();
740 return data;
741 }
742 // Add the bytes of the UTF8 character to a buffer.
743 // Only call this if isComplete() returns true.
744 char *writeUtf8(char *data)
745 {
746 for (unsigned char i = 0; i < count; ++i) {
747 *data++ = bytes[i];
748 }
749 clear();
750 return data;
751 }
752 // Write the bytes in the UTF8 character literally, or, if the
753 // character is not complete, write the escaped bytes.
754 // This is useful to handle the state that remains after handling
755 // all bytes in a buffer.
756 char *write(char *data)
757 {
758 if (isComplete()) {
759 data = writeUtf8(data);
760 } else {
761 data = escapeBytes(data);
762 }
763 return data;
764 }
765};
766}
767
768QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type)
769{
770 const int len = aString.size();
771 if (len == 0) {
772 return aString;
773 }
774
775 QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of len*4
776 result.resize(size: len * 4); // Maximum 4x as long as source string due to \x<ab> escape sequences
777 const char *s = aString.constData();
778 int i = 0;
779 char *data = result.data();
780 char *start = data;
781
782 // Protect leading space
783 if (s[0] == ' ' && type != GroupString) {
784 *data++ = '\\';
785 *data++ = 's';
786 ++i;
787 }
788 Utf8Char utf8;
789
790 for (; i < len; ++i) {
791 switch (s[i]) {
792 default:
793 if (utf8.addByte(b: s[i])) {
794 break;
795 } else {
796 data = utf8.escapeBytes(data);
797 }
798 // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here
799 if (((unsigned char)s[i]) < 32) {
800 goto doEscape;
801 }
802 // GroupString and KeyString should be valid UTF-8, but ValueString
803 // can be a bytearray with non-UTF-8 bytes that should be escaped.
804 if (type == ValueString && ((unsigned char)s[i]) >= 127) {
805 goto doEscape;
806 }
807 *data++ = s[i];
808 break;
809 case '\n':
810 *data++ = '\\';
811 *data++ = 'n';
812 break;
813 case '\t':
814 *data++ = '\\';
815 *data++ = 't';
816 break;
817 case '\r':
818 *data++ = '\\';
819 *data++ = 'r';
820 break;
821 case '\\':
822 *data++ = '\\';
823 *data++ = '\\';
824 break;
825 case '=':
826 if (type != KeyString) {
827 *data++ = s[i];
828 break;
829 }
830 goto doEscape;
831 case '[':
832 case ']':
833 // Above chars are OK to put in *value* strings as plaintext
834 if (type == ValueString) {
835 *data++ = s[i];
836 break;
837 }
838 doEscape:
839 data = escapeByte(data, s: s[i]);
840 break;
841 }
842 if (utf8.isComplete()) {
843 data = utf8.writeUtf8(data);
844 }
845 }
846 data = utf8.write(data);
847 *data = 0;
848 result.resize(size: data - start);
849
850 // Protect trailing space
851 if (result.endsWith(c: ' ') && type != GroupString) {
852 result.replace(index: result.length() - 1, len: 1, s: "\\s");
853 }
854
855 return result;
856}
857
858char KConfigIniBackend::charFromHex(const char *str, const QFile &file, int line)
859{
860 unsigned char ret = 0;
861 for (int i = 0; i < 2; i++) {
862 ret <<= 4;
863 quint8 c = quint8(str[i]);
864
865 if (c >= '0' && c <= '9') {
866 ret |= c - '0';
867 } else if (c >= 'a' && c <= 'f') {
868 ret |= c - 'a' + 0x0a;
869 } else if (c >= 'A' && c <= 'F') {
870 ret |= c - 'A' + 0x0a;
871 } else {
872 QByteArray e(str, 2);
873 e.prepend(s: "\\x");
874 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << "Invalid hex character " << c << " in \\x<nn>-type escape sequence \"" << e.constData()
875 << "\".";
876 return 'x';
877 }
878 }
879 return char(ret);
880}
881
882void KConfigIniBackend::printableToString(BufferFragment *aString, const QFile &file, int line)
883{
884 if (aString->isEmpty() || aString->indexOf(c: '\\') == -1) {
885 return;
886 }
887 aString->trim();
888 int l = aString->length();
889 char *r = aString->data();
890 char *str = r;
891
892 for (int i = 0; i < l; i++, r++) {
893 if (str[i] != '\\') {
894 *r = str[i];
895 } else {
896 // Probable escape sequence
897 ++i;
898 if (i >= l) { // Line ends after backslash - stop.
899 *r = '\\';
900 break;
901 }
902
903 switch (str[i]) {
904 case 's':
905 *r = ' ';
906 break;
907 case 't':
908 *r = '\t';
909 break;
910 case 'n':
911 *r = '\n';
912 break;
913 case 'r':
914 *r = '\r';
915 break;
916 case '\\':
917 *r = '\\';
918 break;
919 case ';':
920 // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string
921 *r = '\\';
922 ++r;
923 *r = ';';
924 break;
925 case ',':
926 // not really an escape sequence, but allowed in .desktop files, don't strip '\,' from the string
927 *r = '\\';
928 ++r;
929 *r = ',';
930 break;
931 case 'x':
932 if (i + 2 < l) {
933 *r = charFromHex(str: str + i + 1, file, line);
934 i += 2;
935 } else {
936 *r = 'x';
937 i = l - 1;
938 }
939 break;
940 default:
941 *r = '\\';
942 qCWarning(KCONFIG_CORE_LOG).noquote() << warningProlog(file, line) << QStringLiteral("Invalid escape sequence: «\\%1»").arg(a: str[i]);
943 }
944 }
945 }
946 aString->truncate(pos: r - aString->constData());
947}
948
949#include "moc_kconfigini_p.cpp"
950

source code of kconfig/src/core/kconfigini.cpp