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

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