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 (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('_') != -1) {
265 entryOptions |= KEntryMap::EntryLocalizedCountry;
266 }
267 }
268 printableToString(line, file, lineNo);
269 if (entryOptions & KEntryMap::EntryRawKey) {
270 QByteArray rawKey;
271 rawKey.reserve(aKey.length() + locale.length() + 2);
272 rawKey.append(aKey);
273 rawKey.append('[').append(locale).append(']');
274 entryMap.setEntry(currentGroup, rawKey, lookup(line, &cache), entryOptions);
275 } else {
276 entryMap.setEntry(currentGroup, lookup(aKey, &cache), lookup(line, &cache), 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(immutableGroups)) {
285 entryMap.setEntry(group, QByteArray(), QByteArray(), 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('\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, false, firstEntry);
391
392 if (!mPrimaryGroup.isNull()) {
393 // write the primary group - it needs to be written before all other groups
394 writeEntries(locale, file, map, false, true, firstEntry);
395 }
396
397 // write all other groups
398 writeEntries(locale, file, map, false, false, firstEntry);
399}
400
401bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options)
402{
403 Q_ASSERT(!filePath().isEmpty());
404
405 KEntryMap writeMap;
406 const bool bGlobal = options & WriteGlobal;
407
408 // First, reparse the file on disk, to merge our changes with the ones done by other apps
409 // Store the result into writeMap.
410 {
411 ParseOptions opts = ParseExpansions;
412 if (bGlobal) {
413 opts |= ParseGlobal;
414 }
415 ParseInfo info = parseConfig(locale, writeMap, opts, true);
416 if (info != ParseOk) { // either there was an error or the file became immutable
417 return false;
418 }
419 }
420
421 for (auto &[key, entry] : entryMap) {
422 if (!key.mKey.isEmpty() && !entry.bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too.
423 continue;
424 }
425
426 // only write entries that have the same "globality" as the file
427 if (entry.bGlobal == bGlobal) {
428 if (entry.bReverted && entry.bOverridesGlobal) {
429 entry.bDeleted = true;
430 writeMap[key] = entry;
431 } else if (entry.bReverted) {
432 writeMap.erase(key);
433 } else if (!entry.bDeleted) {
434 writeMap[key] = entry;
435 } else {
436 KEntryKey defaultKey = key;
437 defaultKey.bDefault = true;
438 if (entryMap.find(defaultKey) == entryMap.end() && !entry.bOverridesGlobal) {
439 writeMap.erase(key); // remove the deleted entry if there is no default
440 // qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal;
441 } else {
442 writeMap[key] = entry; // otherwise write an explicitly deleted entry
443 // qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal;
444 }
445 }
446 entry.bDirty = false;
447 }
448 }
449
450 // now writeMap should contain only entries to be written
451 // so write it out to disk
452
453 // check if file exists
454 QFile::Permissions fileMode = filePath().startsWith(u"/etc/xdg/"_s) ? QFile::ReadUser | QFile::WriteUser | QFile::ReadGroup | QFile::ReadOther //
456
457 bool createNew = true;
458
459 QFileInfo fi(filePath());
460 if (fi.exists()) {
461#ifdef Q_OS_WIN
462 // TODO: getuid does not exist on windows, use GetSecurityInfo and GetTokenInformation instead
463 createNew = false;
464#else
465 if (fi.ownerId() == ::getuid()) {
466 // Preserve file mode if file exists and is owned by user.
467 fileMode = fi.permissions();
468 } else {
469 // File is not owned by user:
470 // Don't create new file but write to existing file instead.
471 createNew = false;
472 }
473#endif
474 }
475
476 if (createNew) {
477 QSaveFile file(filePath());
478 if (!file.open(QIODevice::WriteOnly)) {
479#ifdef Q_OS_ANDROID
480 // HACK: when we are dealing with content:// URIs, QSaveFile has to rely on DirectWrite.
481 // Otherwise this method returns a false and we're done.
482 file.setDirectWriteFallback(true);
483 if (!file.open(QIODevice::WriteOnly)) {
484 qWarning(KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString();
485 return false;
486 }
487#else
488 qWarning(KCONFIG_CORE_LOG) << "Couldn't create a new file:" << filePath() << ". Error:" << file.errorString();
489 return false;
490#endif
491 }
492
493 file.setTextModeEnabled(true); // to get eol translation
494 writeEntries(locale, file, writeMap);
495
496 if (!file.size() && (fileMode == (QFile::ReadUser | QFile::WriteUser))) {
497 // File is empty and doesn't have special permissions: delete it.
498 file.cancelWriting();
499
500 if (fi.exists()) {
501 // also remove the old file in case it existed. this can happen
502 // when we delete all the entries in an existing config file.
503 // if we don't do this, then deletions and revertToDefault's
504 // will mysteriously fail
505 QFile::remove(filePath());
506 }
507 } else {
508 // Normal case: Close the file
509 if (file.commit()) {
510 QFile::setPermissions(filePath(), fileMode);
511 return true;
512 }
513 // Couldn't write. Disk full?
514 qCWarning(KCONFIG_CORE_LOG) << "Couldn't write" << filePath() << ". Disk full?";
515 return false;
516 }
517 } else {
518 QFile f(filePath());
519
520 // Open existing file. *DON'T* create it if it suddenly does not exist!
522 return false;
523 }
524
525 f.setTextModeEnabled(true);
526 writeEntries(locale, f, writeMap);
527 }
528 return true;
529}
530
531bool KConfigIniBackend::isWritable() const
532{
533 const QString filePath = this->filePath();
534 if (filePath.isEmpty()) {
535 return false;
536 }
537
538 QFileInfo file(filePath);
539 if (file.exists()) {
540 return file.isWritable();
541 }
542
543 // If the file does not exist, check if the deepest existing dir is writable
544 QFileInfo dir(file.absolutePath());
545 while (!dir.exists()) {
546 QString parent = dir.absolutePath(); // Go up. Can't use cdUp() on non-existing dirs.
547 if (parent == dir.filePath()) {
548 // no parent
549 return false;
550 }
551 dir.setFile(parent);
552 }
553 return dir.isDir() && dir.isWritable();
554}
555
556QString KConfigIniBackend::nonWritableErrorMessage() const
557{
558 return tr("Configuration file \"%1\" not writable.\n").arg(filePath());
559}
560
561void KConfigIniBackend::createEnclosing()
562{
563 const QString file = filePath();
564 if (file.isEmpty()) {
565 return; // nothing to do
566 }
567
568 // Create the containing dir, maybe it wasn't there
569 QDir().mkpath(QFileInfo(file).absolutePath());
570}
571
572void KConfigIniBackend::setFilePath(const QString &path)
573{
574 if (path.isEmpty()) {
575 return;
576 }
577
578 Q_ASSERT(QDir::isAbsolutePath(path));
579
580 const QFileInfo info(path);
581 if (info.exists()) {
582 setLocalFilePath(info.canonicalFilePath());
583 return;
584 }
585
586 if (QString filePath = info.dir().canonicalPath(); !filePath.isEmpty()) {
587 filePath += QLatin1Char('/') + info.fileName();
588 setLocalFilePath(filePath);
589 } else {
590 setLocalFilePath(path);
591 }
592}
593
594KConfigBase::AccessMode KConfigIniBackend::accessMode() const
595{
596 if (filePath().isEmpty()) {
597 return KConfigBase::NoAccess;
598 }
599
600 if (isWritable()) {
601 return KConfigBase::ReadWrite;
602 }
603
604 return KConfigBase::ReadOnly;
605}
606
607bool KConfigIniBackend::lock()
608{
609 Q_ASSERT(!filePath().isEmpty());
610
611 m_mutex.lock();
612#ifdef Q_OS_ANDROID
613 if (!lockFile) {
614 // handle content Uris properly
615 if (filePath().startsWith(QLatin1String("content://"))) {
616 // we can't create file at an arbitrary location, so use internal storage to create one
617
618 // NOTE: filename can be the same, but because this lock is short lived we may never have a collision
619 lockFile = std::make_unique<QLockFile>(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/")
620 + QFileInfo(filePath()).fileName() + QLatin1String(".lock"));
621 } else {
622 lockFile = std::make_unique<QLockFile>(filePath() + QLatin1String(".lock"));
623 }
624 }
625#else
626 if (!lockFile) {
627 lockFile = std::make_unique<QLockFile>(filePath() + QLatin1String(".lock"));
628 }
629#endif
630
631 if (!lockFile->lock()) {
632 m_mutex.unlock();
633 }
634
635 return lockFile->isLocked();
636}
637
638void KConfigIniBackend::unlock()
639{
640 lockFile->unlock();
641 lockFile = nullptr;
642 m_mutex.unlock();
643}
644
645bool KConfigIniBackend::isLocked() const
646{
647 return lockFile && lockFile->isLocked();
648}
649
650namespace
651{
652// serialize an escaped byte at the end of @param data
653// @param data should have room for 4 bytes
654char *escapeByte(char *data, unsigned char s)
655{
656 static const char nibbleLookup[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
657 *data++ = '\\';
658 *data++ = 'x';
659 *data++ = nibbleLookup[s >> 4];
660 *data++ = nibbleLookup[s & 0x0f];
661 return data;
662}
663
664// Struct that represents a multi-byte UTF-8 character.
665// This struct is used to keep track of bytes that seem to be valid
666// UTF-8.
667struct Utf8Char {
668public:
669 unsigned char bytes[4];
670 unsigned char count;
671 unsigned char charLength;
672
673 Utf8Char()
674 {
675 clear();
676 charLength = 0;
677 }
678 void clear()
679 {
680 count = 0;
681 }
682 // Add a byte to the UTF8 character.
683 // When an additional byte leads to an invalid character, return false.
684 bool addByte(unsigned char b)
685 {
686 if (count == 0) {
687 if (b > 0xc1 && (b & 0xe0) == 0xc0) {
688 charLength = 2;
689 } else if ((b & 0xf0) == 0xe0) {
690 charLength = 3;
691 } else if (b < 0xf5 && (b & 0xf8) == 0xf0) {
692 charLength = 4;
693 } else {
694 return false;
695 }
696 bytes[0] = b;
697 count = 1;
698 } else if (count < 4 && (b & 0xc0) == 0x80) {
699 if (count == 1) {
700 if (charLength == 3 && bytes[0] == 0xe0 && b < 0xa0) {
701 return false; // overlong 3 byte sequence
702 }
703 if (charLength == 4) {
704 if (bytes[0] == 0xf0 && b < 0x90) {
705 return false; // overlong 4 byte sequence
706 }
707 if (bytes[0] == 0xf4 && b > 0x8f) {
708 return false; // Unicode value larger than U+10FFFF
709 }
710 }
711 }
712 bytes[count++] = b;
713 } else {
714 return false;
715 }
716 return true;
717 }
718 // Return true if Utf8Char contains one valid character.
719 bool isComplete() const
720 {
721 return count > 0 && count == charLength;
722 }
723 // Add the bytes in this UTF8 character in escaped form to data.
724 char *escapeBytes(char *data)
725 {
726 for (unsigned char i = 0; i < count; ++i) {
727 data = escapeByte(data, bytes[i]);
728 }
729 clear();
730 return data;
731 }
732 // Add the bytes of the UTF8 character to a buffer.
733 // Only call this if isComplete() returns true.
734 char *writeUtf8(char *data)
735 {
736 for (unsigned char i = 0; i < count; ++i) {
737 *data++ = bytes[i];
738 }
739 clear();
740 return data;
741 }
742 // Write the bytes in the UTF8 character literally, or, if the
743 // character is not complete, write the escaped bytes.
744 // This is useful to handle the state that remains after handling
745 // all bytes in a buffer.
746 char *write(char *data)
747 {
748 if (isComplete()) {
749 data = writeUtf8(data);
750 } else {
751 data = escapeBytes(data);
752 }
753 return data;
754 }
755};
756}
757
758QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type)
759{
760 const int len = aString.size();
761 if (len == 0) {
762 return aString;
763 }
764
765 QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of len*4
766 result.resize(len * 4); // Maximum 4x as long as source string due to \x<ab> escape sequences
767 const char *s = aString.constData();
768 int i = 0;
769 char *data = result.data();
770 char *start = data;
771
772 // Protect leading space
773 if (s[0] == ' ' && type != GroupString) {
774 *data++ = '\\';
775 *data++ = 's';
776 ++i;
777 }
778 Utf8Char utf8;
779
780 for (; i < len; ++i) {
781 switch (s[i]) {
782 default:
783 if (utf8.addByte(s[i])) {
784 break;
785 } else {
786 data = utf8.escapeBytes(data);
787 }
788 // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here
789 if (((unsigned char)s[i]) < 32) {
790 goto doEscape;
791 }
792 // GroupString and KeyString should be valid UTF-8, but ValueString
793 // can be a bytearray with non-UTF-8 bytes that should be escaped.
794 if (type == ValueString && ((unsigned char)s[i]) >= 127) {
795 goto doEscape;
796 }
797 *data++ = s[i];
798 break;
799 case '\n':
800 *data++ = '\\';
801 *data++ = 'n';
802 break;
803 case '\t':
804 *data++ = '\\';
805 *data++ = 't';
806 break;
807 case '\r':
808 *data++ = '\\';
809 *data++ = 'r';
810 break;
811 case '\\':
812 *data++ = '\\';
813 *data++ = '\\';
814 break;
815 case '=':
816 if (type != KeyString) {
817 *data++ = s[i];
818 break;
819 }
820 goto doEscape;
821 case '[':
822 case ']':
823 // Above chars are OK to put in *value* strings as plaintext
824 if (type == ValueString) {
825 *data++ = s[i];
826 break;
827 }
828 doEscape:
829 data = escapeByte(data, s[i]);
830 break;
831 }
832 if (utf8.isComplete()) {
833 data = utf8.writeUtf8(data);
834 }
835 }
836 data = utf8.write(data);
837 *data = 0;
838 result.resize(data - start);
839
840 // Protect trailing space
841 if (result.endsWith(' ') && type != GroupString) {
842 result.replace(result.length() - 1, 1, "\\s");
843 }
844
845 return result;
846}
847
848char KConfigIniBackend::charFromHex(const char *str, const QFile &file, int line)
849{
850 unsigned char ret = 0;
851 for (int i = 0; i < 2; i++) {
852 ret <<= 4;
853 quint8 c = quint8(str[i]);
854
855 if (c >= '0' && c <= '9') {
856 ret |= c - '0';
857 } else if (c >= 'a' && c <= 'f') {
858 ret |= c - 'a' + 0x0a;
859 } else if (c >= 'A' && c <= 'F') {
860 ret |= c - 'A' + 0x0a;
861 } else {
862 QByteArray e(str, 2);
863 e.prepend("\\x");
864 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << "Invalid hex character " << c << " in \\x<nn>-type escape sequence \"" << e.constData()
865 << "\".";
866 return 'x';
867 }
868 }
869 return char(ret);
870}
871
872void KConfigIniBackend::printableToString(QByteArrayView &aString, const QFile &file, int line)
873{
874 if (aString.isEmpty() || aString.indexOf('\\') == -1) {
875 return;
876 }
877 aString = aString.trimmed();
878 int l = aString.size();
879 char *r = const_cast<char *>(aString.data());
880 char *str = r;
881
882 for (int i = 0; i < l; i++, r++) {
883 if (str[i] != '\\') {
884 *r = str[i];
885 } else {
886 // Probable escape sequence
887 ++i;
888 if (i >= l) { // Line ends after backslash - stop.
889 *r = '\\';
890 break;
891 }
892
893 switch (str[i]) {
894 case 's':
895 *r = ' ';
896 break;
897 case 't':
898 *r = '\t';
899 break;
900 case 'n':
901 *r = '\n';
902 break;
903 case 'r':
904 *r = '\r';
905 break;
906 case '\\':
907 *r = '\\';
908 break;
909 case ';':
910 // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string
911 *r = '\\';
912 ++r;
913 *r = ';';
914 break;
915 case ',':
916 // not really an escape sequence, but allowed in .desktop files, don't strip '\,' from the string
917 *r = '\\';
918 ++r;
919 *r = ',';
920 break;
921 case 'x':
922 if (i + 2 < l) {
923 *r = charFromHex(str + i + 1, file, line);
924 i += 2;
925 } else {
926 *r = 'x';
927 i = l - 1;
928 }
929 break;
930 default:
931 *r = '\\';
932 qCWarning(KCONFIG_CORE_LOG).noquote() << warningProlog(file, line) << QStringLiteral("Invalid escape sequence: «\\%1»").arg(str[i]);
933 }
934 }
935 }
936 aString.truncate(r - aString.constData());
937}
938
939QString KConfigIniBackend::filePath() const
940{
941 return mLocalFilePath;
942}
943
944void KConfigIniBackend::setLocalFilePath(const QString &file)
945{
946 mLocalFilePath = file;
947}
948
949void KConfigIniBackend::setPrimaryGroup(const QString &group)
950{
951 mPrimaryGroup = group;
952}
953
954#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
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
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 May 2 2025 12:04:35 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.