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

KDE's Doxygen guidelines are available online.