KConfig

kconfigini.cpp
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
36{
37 auto it = cache->constFind(fragment);
38 if (it != cache->constEnd()) {
39 return it.value();
40 }
41
42 return cache->insert(fragment, 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(line).arg(file.fileName());
50}
51
52KConfigIniBackend::KConfigIniBackend()
53{
54}
55
56KConfigIniBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options)
57{
58 return parseConfig(currentLocale, entryMap, options, 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());
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('_');
87 const QByteArray currentLanguage = langIdx >= 0 ? currentLocale.left(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.
99 cache.reserve(4096);
100
101 while (!contents.isEmpty()) {
102 QByteArrayView line;
103 if (const auto idx = contents.indexOf('\n'); idx < 0) {
104 line = contents;
105 contents = {};
106 } else {
107 line = contents.left(idx);
108 contents = contents.mid(idx + 1);
109 }
110 line = line.trimmed();
111 ++lineNo;
112
113 // skip empty lines and lines beginning with '#'
114 if (line.isEmpty() || line.at(0) == '#') {
115 continue;
116 }
117
118 if (line.at(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, lineNo) << "Invalid group header.";
129 // XXX maybe reset the current group here?
130 goto next_line;
131 }
132 if (line.at(end) == ']') {
133 break;
134 }
135 ++end;
136 }
137 /* clang-format off */
138 if (end + 1 == line.length()
139 && start + 2 == end
140 && line.at(start) == '$'
141 && line.at(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(start, end - start);
152 printableToString(namePart, file, lineNo);
153 newGroup += namePart.toByteArray();
154 }
155 } while ((start = end + 2) <= line.length() && line.at(end + 1) == '[');
156 currentGroup = QString::fromUtf8(newGroup);
157
158 groupSkip = entryMap.getEntryOption(currentGroup, {}, {}, 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(currentGroup);
169 }
170 } else {
171 if (groupSkip && !bDefault) {
172 continue; // skip entry
173 }
174
175 QByteArrayView aKey;
176 int eqpos = line.indexOf('=');
177 if (eqpos < 0) {
178 aKey = line;
179 line = {};
180 } else {
181 QByteArrayView temp = line.left(eqpos);
182 aKey = temp.trimmed();
183 line = line.mid(eqpos + 1);
184 line = line.trimmed();
185 }
186 if (aKey.isEmpty()) {
187 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, 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('[')) >= 0) {
199 int end = aKey.indexOf(']', start);
200 if (end < 0) {
201 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (missing ']')";
202 goto next_line;
203 } else if (end > start + 1 && aKey.at(start + 1) == '$') { // found option(s)
204 int i = start + 2;
205 while (i < end) {
206 switch (aKey.at(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(start);
220 printableToString(aKey, file, lineNo);
221 entryMap.setEntry(currentGroup, aKey.toByteArray(), QByteArray(), 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, lineNo) << "Invalid entry (second locale!?)";
231 goto next_line;
232 }
233
234 locale = aKey.mid(start + 1, end - start - 1);
235 }
236 aKey.truncate(start);
237 }
238 if (eqpos < 0) { // Do this here after [$d] was checked
239 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (missing '=')";
240 continue;
241 }
242 printableToString(aKey, file, lineNo);
243 if (!locale.isEmpty()) {
244 if (locale != currentLocale && locale != currentLanguage) {
245 // backward compatibility. C == en_US
246 if (locale.at(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 (!(entryOptions & KEntryMap::EntryRawKey)) {
257 printableToString(aKey, file, lineNo);
258 }
259
260 if (options & ParseGlobal) {
261 entryOptions |= KEntryMap::EntryGlobal;
262 }
263 if (bDefault) {
264 entryOptions |= KEntryMap::EntryDefault;
265 }
266 if (!locale.isNull()) {
267 entryOptions |= KEntryMap::EntryLocalized;
268 if (locale.indexOf('_') != -1) {
269 entryOptions |= KEntryMap::EntryLocalizedCountry;
270 }
271 }
272 printableToString(line, file, lineNo);
273 if (entryOptions & KEntryMap::EntryRawKey) {
274 QByteArray rawKey;
275 rawKey.reserve(aKey.length() + locale.length() + 2);
276 rawKey.append(aKey);
277 rawKey.append('[').append(locale).append(']');
278 entryMap.setEntry(currentGroup, rawKey, lookup(line, &cache), entryOptions);
279 } else {
280 entryMap.setEntry(currentGroup, lookup(aKey, &cache), lookup(line, &cache), entryOptions);
281 }
282 }
283 next_line:
284 continue;
285 }
286
287 // now make sure immutable groups are marked immutable
288 for (const QString &group : std::as_const(immutableGroups)) {
289 entryMap.setEntry(group, QByteArray(), QByteArray(), KEntryMap::EntryImmutable);
290 }
291
292 return fileOptionImmutable ? ParseImmutable : ParseOk;
293}
294
295void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map, bool defaultGroup, bool &firstEntry)
296{
297 QString currentGroup;
298 bool groupIsImmutable = false;
299 for (const auto &[key, entry] : map) {
300 // Either process the default group or all others
301 if ((key.mGroup != QStringLiteral("<default>")) == defaultGroup) {
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('\n');
315 }
316 currentGroup = key.mGroup;
317 for (int start = 0, end;; start = end + 1) {
318 file.putChar('[');
319 end = currentGroup.indexOf(QLatin1Char('\x1d'), start);
320 if (end < 0) {
321 int cgl = currentGroup.length();
322 if (currentGroup.at(start) == QLatin1Char('$') && cgl - start <= 10) {
323 for (int i = start + 1; i < cgl; i++) {
324 const QChar c = currentGroup.at(i);
325 if (c < QLatin1Char('a') || c > QLatin1Char('z')) {
326 goto nope;
327 }
328 }
329 file.write("\\x24");
330 ++start;
331 }
332 nope:
333 // TODO: make stringToPrintable also process QString, to save the conversion here and below
334 file.write(stringToPrintable(QStringView(currentGroup).mid(start).toUtf8(), GroupString));
335 file.putChar(']');
336 if (groupIsImmutable) {
337 file.write("[$i]", 4);
338 }
339 file.putChar('\n');
340 break;
341 } else {
342 file.write(stringToPrintable(QStringView(currentGroup).mid(start, end - start).toUtf8(), GroupString));
343 file.putChar(']');
344 }
345 }
346 }
347
348 firstEntry = false;
349 // it is data for a group
350
351 if (key.bRaw) { // unprocessed key with attached locale from merge
352 file.write(key.mKey);
353 } else {
354 file.write(stringToPrintable(key.mKey, KeyString)); // Key
355 if (key.bLocal && locale != "C") { // 'C' locale == untranslated
356 file.putChar('[');
357 file.write(locale); // locale tag
358 file.putChar(']');
359 }
360 }
361 if (currentEntry.bDeleted) {
362 if (currentEntry.bImmutable) {
363 file.write("[$di]", 5); // Deleted + immutable
364 } else {
365 file.write("[$d]", 4); // Deleted
366 }
367 } else {
368 if (currentEntry.bImmutable || currentEntry.bExpand) {
369 file.write("[$", 2);
370 if (currentEntry.bImmutable) {
371 file.putChar('i');
372 }
373 if (currentEntry.bExpand) {
374 file.putChar('e');
375 }
376 file.putChar(']');
377 }
378 file.putChar('=');
379 file.write(stringToPrintable(currentEntry.mValue, ValueString));
380 }
381 file.putChar('\n');
382 }
383}
384
385void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map)
386{
387 bool firstEntry = true;
388
389 // write default group
390 writeEntries(locale, file, map, true, firstEntry);
391
392 // write all other groups
393 writeEntries(locale, file, map, false, firstEntry);
394}
395
396bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options)
397{
398 Q_ASSERT(!filePath().isEmpty());
399
400 KEntryMap writeMap;
401 const bool bGlobal = options & WriteGlobal;
402
403 // First, reparse the file on disk, to merge our changes with the ones done by other apps
404 // Store the result into writeMap.
405 {
406 ParseOptions opts = ParseExpansions;
407 if (bGlobal) {
408 opts |= ParseGlobal;
409 }
410 ParseInfo info = parseConfig(locale, writeMap, opts, true);
411 if (info != ParseOk) { // either there was an error or the file became immutable
412 return false;
413 }
414 }
415
416 for (auto &[key, entry] : entryMap) {
417 if (!key.mKey.isEmpty() && !entry.bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too.
418 continue;
419 }
420
421 // only write entries that have the same "globality" as the file
422 if (entry.bGlobal == bGlobal) {
423 if (entry.bReverted && entry.bOverridesGlobal) {
424 entry.bDeleted = true;
425 writeMap[key] = entry;
426 } else if (entry.bReverted) {
427 writeMap.erase(key);
428 } else if (!entry.bDeleted) {
429 writeMap[key] = entry;
430 } else {
431 KEntryKey defaultKey = key;
432 defaultKey.bDefault = true;
433 if (entryMap.find(defaultKey) == entryMap.end() && !entry.bOverridesGlobal) {
434 writeMap.erase(key); // remove the deleted entry if there is no default
435 // qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal;
436 } else {
437 writeMap[key] = entry; // otherwise write an explicitly deleted entry
438 // qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal;
439 }
440 }
441 entry.bDirty = false;
442 }
443 }
444
445 // now writeMap should contain only entries to be written
446 // so write it out to disk
447
448 // check if file exists
449 QFile::Permissions fileMode = filePath().startsWith(u"/etc/xdg/"_s) ? QFile::ReadUser | QFile::WriteUser | QFile::ReadGroup | QFile::ReadOther //
451
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(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(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, 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(filePath());
501 }
502 } else {
503 // Normal case: Close the file
504 if (file.commit()) {
505 QFile::setPermissions(filePath(), 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(QFile::encodeName(filePath()).constData(), O_WRONLY | O_TRUNC);
516 if (fd < 0) {
517 return false;
518 }
519 QFile f;
520 if (!f.open(fd, QIODevice::WriteOnly)) {
521 QT_CLOSE(fd);
522 return false;
523 }
524 writeEntries(locale, f, writeMap);
525 f.close();
526 QT_CLOSE(fd);
527#else
528 QFile f(filePath());
529 // XXX This is broken - it DOES create the file if it is suddenly gone.
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("Configuration file \"%1\" not writable.\n").arg(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(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 = std::make_unique<QLockFile>(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/")
629 + QFileInfo(filePath()).fileName() + QLatin1String(".lock"));
630 } else {
631 lockFile = std::make_unique<QLockFile>(filePath() + QLatin1String(".lock"));
632 }
633 }
634#else
635 if (!lockFile) {
636 lockFile = std::make_unique<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 lockFile = nullptr;
651 m_mutex.unlock();
652}
653
654bool KConfigIniBackend::isLocked() const
655{
656 return lockFile && lockFile->isLocked();
657}
658
659namespace
660{
661// serialize an escaped byte at the end of @param data
662// @param data should have room for 4 bytes
663char *escapeByte(char *data, unsigned char s)
664{
665 static const char nibbleLookup[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
666 *data++ = '\\';
667 *data++ = 'x';
668 *data++ = nibbleLookup[s >> 4];
669 *data++ = nibbleLookup[s & 0x0f];
670 return data;
671}
672
673// Struct that represents a multi-byte UTF-8 character.
674// This struct is used to keep track of bytes that seem to be valid
675// UTF-8.
676struct Utf8Char {
677public:
678 unsigned char bytes[4];
679 unsigned char count;
680 unsigned char charLength;
681
682 Utf8Char()
683 {
684 clear();
685 charLength = 0;
686 }
687 void clear()
688 {
689 count = 0;
690 }
691 // Add a byte to the UTF8 character.
692 // When an additional byte leads to an invalid character, return false.
693 bool addByte(unsigned char b)
694 {
695 if (count == 0) {
696 if (b > 0xc1 && (b & 0xe0) == 0xc0) {
697 charLength = 2;
698 } else if ((b & 0xf0) == 0xe0) {
699 charLength = 3;
700 } else if (b < 0xf5 && (b & 0xf8) == 0xf0) {
701 charLength = 4;
702 } else {
703 return false;
704 }
705 bytes[0] = b;
706 count = 1;
707 } else if (count < 4 && (b & 0xc0) == 0x80) {
708 if (count == 1) {
709 if (charLength == 3 && bytes[0] == 0xe0 && b < 0xa0) {
710 return false; // overlong 3 byte sequence
711 }
712 if (charLength == 4) {
713 if (bytes[0] == 0xf0 && b < 0x90) {
714 return false; // overlong 4 byte sequence
715 }
716 if (bytes[0] == 0xf4 && b > 0x8f) {
717 return false; // Unicode value larger than U+10FFFF
718 }
719 }
720 }
721 bytes[count++] = b;
722 } else {
723 return false;
724 }
725 return true;
726 }
727 // Return true if Utf8Char contains one valid character.
728 bool isComplete() const
729 {
730 return count > 0 && count == charLength;
731 }
732 // Add the bytes in this UTF8 character in escaped form to data.
733 char *escapeBytes(char *data)
734 {
735 for (unsigned char i = 0; i < count; ++i) {
736 data = escapeByte(data, bytes[i]);
737 }
738 clear();
739 return data;
740 }
741 // Add the bytes of the UTF8 character to a buffer.
742 // Only call this if isComplete() returns true.
743 char *writeUtf8(char *data)
744 {
745 for (unsigned char i = 0; i < count; ++i) {
746 *data++ = bytes[i];
747 }
748 clear();
749 return data;
750 }
751 // Write the bytes in the UTF8 character literally, or, if the
752 // character is not complete, write the escaped bytes.
753 // This is useful to handle the state that remains after handling
754 // all bytes in a buffer.
755 char *write(char *data)
756 {
757 if (isComplete()) {
758 data = writeUtf8(data);
759 } else {
760 data = escapeBytes(data);
761 }
762 return data;
763 }
764};
765}
766
767QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type)
768{
769 const int len = aString.size();
770 if (len == 0) {
771 return aString;
772 }
773
774 QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of len*4
775 result.resize(len * 4); // Maximum 4x as long as source string due to \x<ab> escape sequences
776 const char *s = aString.constData();
777 int i = 0;
778 char *data = result.data();
779 char *start = data;
780
781 // Protect leading space
782 if (s[0] == ' ' && type != GroupString) {
783 *data++ = '\\';
784 *data++ = 's';
785 ++i;
786 }
787 Utf8Char utf8;
788
789 for (; i < len; ++i) {
790 switch (s[i]) {
791 default:
792 if (utf8.addByte(s[i])) {
793 break;
794 } else {
795 data = utf8.escapeBytes(data);
796 }
797 // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here
798 if (((unsigned char)s[i]) < 32) {
799 goto doEscape;
800 }
801 // GroupString and KeyString should be valid UTF-8, but ValueString
802 // can be a bytearray with non-UTF-8 bytes that should be escaped.
803 if (type == ValueString && ((unsigned char)s[i]) >= 127) {
804 goto doEscape;
805 }
806 *data++ = s[i];
807 break;
808 case '\n':
809 *data++ = '\\';
810 *data++ = 'n';
811 break;
812 case '\t':
813 *data++ = '\\';
814 *data++ = 't';
815 break;
816 case '\r':
817 *data++ = '\\';
818 *data++ = 'r';
819 break;
820 case '\\':
821 *data++ = '\\';
822 *data++ = '\\';
823 break;
824 case '=':
825 if (type != KeyString) {
826 *data++ = s[i];
827 break;
828 }
829 goto doEscape;
830 case '[':
831 case ']':
832 // Above chars are OK to put in *value* strings as plaintext
833 if (type == ValueString) {
834 *data++ = s[i];
835 break;
836 }
837 doEscape:
838 data = escapeByte(data, s[i]);
839 break;
840 }
841 if (utf8.isComplete()) {
842 data = utf8.writeUtf8(data);
843 }
844 }
845 data = utf8.write(data);
846 *data = 0;
847 result.resize(data - start);
848
849 // Protect trailing space
850 if (result.endsWith(' ') && type != GroupString) {
851 result.replace(result.length() - 1, 1, "\\s");
852 }
853
854 return result;
855}
856
857char KConfigIniBackend::charFromHex(const char *str, const QFile &file, int line)
858{
859 unsigned char ret = 0;
860 for (int i = 0; i < 2; i++) {
861 ret <<= 4;
862 quint8 c = quint8(str[i]);
863
864 if (c >= '0' && c <= '9') {
865 ret |= c - '0';
866 } else if (c >= 'a' && c <= 'f') {
867 ret |= c - 'a' + 0x0a;
868 } else if (c >= 'A' && c <= 'F') {
869 ret |= c - 'A' + 0x0a;
870 } else {
871 QByteArray e(str, 2);
872 e.prepend("\\x");
873 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << "Invalid hex character " << c << " in \\x<nn>-type escape sequence \"" << e.constData()
874 << "\".";
875 return 'x';
876 }
877 }
878 return char(ret);
879}
880
881void KConfigIniBackend::printableToString(QByteArrayView &aString, const QFile &file, int line)
882{
883 if (aString.isEmpty() || aString.indexOf('\\') == -1) {
884 return;
885 }
886 aString = aString.trimmed();
887 int l = aString.size();
888 char *r = const_cast<char *>(aString.data());
889 char *str = r;
890
891 for (int i = 0; i < l; i++, r++) {
892 if (str[i] != '\\') {
893 *r = str[i];
894 } else {
895 // Probable escape sequence
896 ++i;
897 if (i >= l) { // Line ends after backslash - stop.
898 *r = '\\';
899 break;
900 }
901
902 switch (str[i]) {
903 case 's':
904 *r = ' ';
905 break;
906 case 't':
907 *r = '\t';
908 break;
909 case 'n':
910 *r = '\n';
911 break;
912 case 'r':
913 *r = '\r';
914 break;
915 case '\\':
916 *r = '\\';
917 break;
918 case ';':
919 // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string
920 *r = '\\';
921 ++r;
922 *r = ';';
923 break;
924 case ',':
925 // not really an escape sequence, but allowed in .desktop files, don't strip '\,' from the string
926 *r = '\\';
927 ++r;
928 *r = ',';
929 break;
930 case 'x':
931 if (i + 2 < l) {
932 *r = charFromHex(str + i + 1, file, line);
933 i += 2;
934 } else {
935 *r = 'x';
936 i = l - 1;
937 }
938 break;
939 default:
940 *r = '\\';
941 qCWarning(KCONFIG_CORE_LOG).noquote() << warningProlog(file, line) << QStringLiteral("Invalid escape sequence: «\\%1»").arg(str[i]);
942 }
943 }
944 }
945 aString.truncate(r - aString.constData());
946}
947
948QString KConfigIniBackend::filePath() const
949{
950 return mLocalFilePath;
951}
952
953void KConfigIniBackend::setLocalFilePath(const QString &file)
954{
955 mLocalFilePath = file;
956}
957
958#include "moc_kconfigini_p.cpp"
AccessMode
Possible return values for accessMode().
Q_SCRIPTABLE QString start(QString train="")
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
QString path(const QString &relativePath)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KGuiItem clear()
QByteArray & append(QByteArrayView data)
const char * constData() const const
char * data()
bool endsWith(QByteArrayView bv) const const
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
bool isEmpty() const const
QByteArray left(qsizetype len) const const
qsizetype length() const const
QByteArray & replace(QByteArrayView before, QByteArrayView after)
void reserve(qsizetype size)
void resize(qsizetype newSize, char c)
qsizetype size() const const
QByteArrayView left(qsizetype length) const const
QByteArrayView mid(qsizetype start, qsizetype length) const const
char at(qsizetype n) const const
const_pointer constData() const const
const_pointer data() const const
qsizetype indexOf(QByteArrayView bv, qsizetype from) const const
bool isEmpty() const const
bool isNull() const const
qsizetype lastIndexOf(QByteArrayView bv) const const
qsizetype length() const const
qsizetype size() const const
QByteArray toByteArray() const const
QByteArrayView trimmed() const const
void truncate(qsizetype length)
bool isAbsolutePath(const QString &path)
bool mkpath(const QString &dirPath) const const
QByteArray encodeName(const QString &fileName)
bool exists(const QString &fileName)
virtual QString fileName() const const override
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
bool remove()
virtual bool setPermissions(Permissions permissions) override
typedef Permissions
virtual void close() override
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
iterator insert(const Key &key, const T &value)
void reserve(qsizetype size)
QString errorString() const const
bool isWritable() const const
virtual bool open(QIODeviceBase::OpenMode mode)
bool putChar(char c)
QByteArray readAll()
void setTextModeEnabled(bool enabled)
virtual qint64 size() const const
qint64 write(const QByteArray &data)
void append(QList< T > &&value)
QString writableLocation(StandardLocation type)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype length() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:59:53 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.