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 "bufferfragment_p.h"
13#include "kconfig.h"
14#include "kconfig_core_log_settings.h"
15#include "kconfigbackend_p.h"
16#include "kconfigdata_p.h"
17
18#include <QDateTime>
19#include <QDebug>
20#include <QDir>
21#include <QFile>
22#include <QFileInfo>
23#include <QLockFile>
24#include <QSaveFile>
25#include <qplatformdefs.h>
26
27#ifndef Q_OS_WIN
28#include <unistd.h> // getuid, close
29#endif
30#include <fcntl.h> // open
31#include <sys/types.h> // uid_t
32
33KCONFIGCORE_EXPORT bool kde_kiosk_exception = false; // flag to disable kiosk restrictions
34
35static QByteArray lookup(const KConfigIniBackend::BufferFragment fragment, QHash<KConfigIniBackend::BufferFragment, QByteArray> *cache)
36{
37 auto it = cache->constFind(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 : KConfigBackend()
54 , lockFile(nullptr)
55{
56}
57
58KConfigIniBackend::~KConfigIniBackend()
59{
60}
61
62KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options)
63{
64 return parseConfig(currentLocale, entryMap, options, false);
65}
66
67// merging==true is the merging that happens at the beginning of writeConfig:
68// merge changes in the on-disk file with the changes in the KConfig object.
69KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options, bool merging)
70{
71 if (filePath().isEmpty()) {
72 return ParseOk;
73 }
74
75 QFile file(filePath());
77 return file.exists() ? ParseOpenError : ParseOk;
78 }
79
80 QList<QString> immutableGroups;
81
82 bool fileOptionImmutable = false;
83 bool groupOptionImmutable = false;
84 bool groupSkip = false;
85
86 int lineNo = 0;
87 // on systems using \r\n as end of line, \r will be taken care of by
88 // trim() below
89 QByteArray buffer = file.readAll();
90 BufferFragment contents(buffer.data(), buffer.size());
91 unsigned int len = contents.length();
92 unsigned int startOfLine = 0;
93
94 const int langIdx = currentLocale.indexOf('_');
95 const QByteArray currentLanguage = langIdx >= 0 ? currentLocale.left(langIdx) : currentLocale;
96
97 QString currentGroup = QStringLiteral("<default>");
98 bool bDefault = options & ParseDefaults;
99 bool allowExecutableValues = options & ParseExpansions;
100
101 // Reduce memory overhead by making use of implicit sharing
102 // This assumes that config files contain only a small amount of
103 // different fragments which are repeated often.
104 // This is often the case, especially sub groups will all have
105 // the same list of keys and similar values as well.
107 cache.reserve(4096);
108
109 while (startOfLine < len) {
110 BufferFragment line = contents.split('\n', &startOfLine);
111 line.trim();
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 BufferFragment 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 BufferFragment aKey;
177 int eqpos = line.indexOf('=');
178 if (eqpos < 0) {
179 aKey = line;
180 line.clear();
181 } else {
182 BufferFragment temp = line.left(eqpos);
183 temp.trim();
184 aKey = temp;
185 line.truncateLeft(eqpos + 1);
186 line.trim();
187 }
188 if (aKey.isEmpty()) {
189 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (empty key)";
190 continue;
191 }
192
193 KEntryMap::EntryOptions entryOptions = {};
194 if (groupOptionImmutable) {
195 entryOptions |= KEntryMap::EntryImmutable;
196 }
197
198 BufferFragment locale;
199 int start;
200 while ((start = aKey.lastIndexOf('[')) >= 0) {
201 int end = aKey.indexOf(']', start);
202 if (end < 0) {
203 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (missing ']')";
204 goto next_line;
205 } else if (end > start + 1 && aKey.at(start + 1) == '$') { // found option(s)
206 int i = start + 2;
207 while (i < end) {
208 switch (aKey.at(i)) {
209 case 'i':
210 if (!kde_kiosk_exception) {
211 entryOptions |= KEntryMap::EntryImmutable;
212 }
213 break;
214 case 'e':
215 if (allowExecutableValues) {
216 entryOptions |= KEntryMap::EntryExpansion;
217 }
218 break;
219 case 'd':
220 entryOptions |= KEntryMap::EntryDeleted;
221 aKey.truncate(start);
222 printableToString(&aKey, file, lineNo);
223 entryMap.setEntry(currentGroup, aKey.toByteArray(), QByteArray(), entryOptions);
224 goto next_line;
225 default:
226 break;
227 }
228 ++i;
229 }
230 } else { // found a locale
231 if (!locale.isNull()) {
232 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (second locale!?)";
233 goto next_line;
234 }
235
236 locale = aKey.mid(start + 1, end - start - 1);
237 }
238 aKey.truncate(start);
239 }
240 if (eqpos < 0) { // Do this here after [$d] was checked
241 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (missing '=')";
242 continue;
243 }
244 printableToString(&aKey, file, lineNo);
245 if (!locale.isEmpty()) {
246 if (locale != currentLocale && locale != currentLanguage) {
247 // backward compatibility. C == en_US
248 if (locale.at(0) != 'C' || currentLocale != "en_US") {
249 if (merging) {
250 entryOptions |= KEntryMap::EntryRawKey;
251 } else {
252 goto next_line; // skip this entry if we're not merging
253 }
254 }
255 }
256 }
257
258 if (!(entryOptions & KEntryMap::EntryRawKey)) {
259 printableToString(&aKey, file, lineNo);
260 }
261
262 if (options & ParseGlobal) {
263 entryOptions |= KEntryMap::EntryGlobal;
264 }
265 if (bDefault) {
266 entryOptions |= KEntryMap::EntryDefault;
267 }
268 if (!locale.isNull()) {
269 entryOptions |= KEntryMap::EntryLocalized;
270 if (locale.indexOf('_') != -1) {
271 entryOptions |= KEntryMap::EntryLocalizedCountry;
272 }
273 }
274 printableToString(&line, file, lineNo);
275 if (entryOptions & KEntryMap::EntryRawKey) {
276 QByteArray rawKey;
277 rawKey.reserve(aKey.length() + locale.length() + 2);
278 rawKey.append(aKey.toVolatileByteArray());
279 rawKey.append('[').append(locale.toVolatileByteArray()).append(']');
280 entryMap.setEntry(currentGroup, rawKey, lookup(line, &cache), entryOptions);
281 } else {
282 entryMap.setEntry(currentGroup, lookup(aKey, &cache), lookup(line, &cache), entryOptions);
283 }
284 }
285 next_line:
286 continue;
287 }
288
289 // now make sure immutable groups are marked immutable
290 for (const QString &group : std::as_const(immutableGroups)) {
291 entryMap.setEntry(group, QByteArray(), QByteArray(), KEntryMap::EntryImmutable);
292 }
293
294 return fileOptionImmutable ? ParseImmutable : ParseOk;
295}
296
297void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map, bool defaultGroup, bool &firstEntry)
298{
299 QString currentGroup;
300 bool groupIsImmutable = false;
301 for (const auto &[key, entry] : map) {
302 // Either process the default group or all others
303 if ((key.mGroup != QStringLiteral("<default>")) == defaultGroup) {
304 continue; // skip
305 }
306
307 // the only thing we care about groups is, is it immutable?
308 if (key.mKey.isNull()) {
309 groupIsImmutable = entry.bImmutable;
310 continue; // skip
311 }
312
313 const KEntry &currentEntry = entry;
314 if (!defaultGroup && currentGroup != key.mGroup) {
315 if (!firstEntry) {
316 file.putChar('\n');
317 }
318 currentGroup = key.mGroup;
319 for (int start = 0, end;; start = end + 1) {
320 file.putChar('[');
321 end = currentGroup.indexOf(QLatin1Char('\x1d'), start);
322 if (end < 0) {
323 int cgl = currentGroup.length();
324 if (currentGroup.at(start) == QLatin1Char('$') && cgl - start <= 10) {
325 for (int i = start + 1; i < cgl; i++) {
326 const QChar c = currentGroup.at(i);
327 if (c < QLatin1Char('a') || c > QLatin1Char('z')) {
328 goto nope;
329 }
330 }
331 file.write("\\x24");
332 ++start;
333 }
334 nope:
335 // TODO: make stringToPrintable also process QString, to save the conversion here and below
336 file.write(stringToPrintable(QStringView(currentGroup).mid(start).toUtf8(), GroupString));
337 file.putChar(']');
338 if (groupIsImmutable) {
339 file.write("[$i]", 4);
340 }
341 file.putChar('\n');
342 break;
343 } else {
344 file.write(stringToPrintable(QStringView(currentGroup).mid(start, end - start).toUtf8(), GroupString));
345 file.putChar(']');
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(key.mKey);
355 } else {
356 file.write(stringToPrintable(key.mKey, KeyString)); // Key
357 if (key.bLocal && locale != "C") { // 'C' locale == untranslated
358 file.putChar('[');
359 file.write(locale); // locale tag
360 file.putChar(']');
361 }
362 }
363 if (currentEntry.bDeleted) {
364 if (currentEntry.bImmutable) {
365 file.write("[$di]", 5); // Deleted + immutable
366 } else {
367 file.write("[$d]", 4); // Deleted
368 }
369 } else {
370 if (currentEntry.bImmutable || currentEntry.bExpand) {
371 file.write("[$", 2);
372 if (currentEntry.bImmutable) {
373 file.putChar('i');
374 }
375 if (currentEntry.bExpand) {
376 file.putChar('e');
377 }
378 file.putChar(']');
379 }
380 file.putChar('=');
381 file.write(stringToPrintable(currentEntry.mValue, ValueString));
382 }
383 file.putChar('\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, true, firstEntry);
393
394 // write all other groups
395 writeEntries(locale, file, map, false, firstEntry);
396}
397
398bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options)
399{
400 Q_ASSERT(!filePath().isEmpty());
401
402 KEntryMap writeMap;
403 const bool bGlobal = options & WriteGlobal;
404
405 // First, reparse the file on disk, to merge our changes with the ones done by other apps
406 // Store the result into writeMap.
407 {
408 ParseOptions opts = ParseExpansions;
409 if (bGlobal) {
410 opts |= ParseGlobal;
411 }
412 ParseInfo info = parseConfig(locale, writeMap, opts, true);
413 if (info != ParseOk) { // either there was an error or the file became immutable
414 return false;
415 }
416 }
417
418 for (auto &[key, entry] : entryMap) {
419 if (!key.mKey.isEmpty() && !entry.bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too.
420 continue;
421 }
422
423 // only write entries that have the same "globality" as the file
424 if (entry.bGlobal == bGlobal) {
425 if (entry.bReverted && entry.bOverridesGlobal) {
426 entry.bDeleted = true;
427 writeMap[key] = entry;
428 } else if (entry.bReverted) {
429 writeMap.erase(key);
430 } else if (!entry.bDeleted) {
431 writeMap[key] = entry;
432 } else {
433 KEntryKey defaultKey = key;
434 defaultKey.bDefault = true;
435 if (entryMap.find(defaultKey) == entryMap.end() && !entry.bOverridesGlobal) {
436 writeMap.erase(key); // remove the deleted entry if there is no default
437 // qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal;
438 } else {
439 writeMap[key] = entry; // otherwise write an explicitly deleted entry
440 // qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal;
441 }
442 }
443 entry.bDirty = false;
444 }
445 }
446
447 // now writeMap should contain only entries to be written
448 // so write it out to disk
449
450 // check if file exists
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
629 + QFileInfo(filePath()).fileName() + QLatin1String(".lock"));
630 } else {
631 lockFile = new QLockFile(filePath() + QLatin1String(".lock"));
632 }
633 }
634#else
635 if (!lockFile) {
636 lockFile = new QLockFile(filePath() + QLatin1String(".lock"));
637 }
638#endif
639
640 if (!lockFile->lock()) {
641 m_mutex.unlock();
642 }
643
644 return lockFile->isLocked();
645}
646
647void KConfigIniBackend::unlock()
648{
649 lockFile->unlock();
650 delete lockFile;
651 lockFile = nullptr;
652 m_mutex.unlock();
653}
654
655bool KConfigIniBackend::isLocked() const
656{
657 return lockFile && lockFile->isLocked();
658}
659
660namespace
661{
662// serialize an escaped byte at the end of @param data
663// @param data should have room for 4 bytes
664char *escapeByte(char *data, unsigned char s)
665{
666 static const char nibbleLookup[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
667 *data++ = '\\';
668 *data++ = 'x';
669 *data++ = nibbleLookup[s >> 4];
670 *data++ = nibbleLookup[s & 0x0f];
671 return data;
672}
673
674// Struct that represents a multi-byte UTF-8 character.
675// This struct is used to keep track of bytes that seem to be valid
676// UTF-8.
677struct Utf8Char {
678public:
679 unsigned char bytes[4];
680 unsigned char count;
681 unsigned char charLength;
682
683 Utf8Char()
684 {
685 clear();
686 charLength = 0;
687 }
688 void clear()
689 {
690 count = 0;
691 }
692 // Add a byte to the UTF8 character.
693 // When an additional byte leads to an invalid character, return false.
694 bool addByte(unsigned char b)
695 {
696 if (count == 0) {
697 if (b > 0xc1 && (b & 0xe0) == 0xc0) {
698 charLength = 2;
699 } else if ((b & 0xf0) == 0xe0) {
700 charLength = 3;
701 } else if (b < 0xf5 && (b & 0xf8) == 0xf0) {
702 charLength = 4;
703 } else {
704 return false;
705 }
706 bytes[0] = b;
707 count = 1;
708 } else if (count < 4 && (b & 0xc0) == 0x80) {
709 if (count == 1) {
710 if (charLength == 3 && bytes[0] == 0xe0 && b < 0xa0) {
711 return false; // overlong 3 byte sequence
712 }
713 if (charLength == 4) {
714 if (bytes[0] == 0xf0 && b < 0x90) {
715 return false; // overlong 4 byte sequence
716 }
717 if (bytes[0] == 0xf4 && b > 0x8f) {
718 return false; // Unicode value larger than U+10FFFF
719 }
720 }
721 }
722 bytes[count++] = b;
723 } else {
724 return false;
725 }
726 return true;
727 }
728 // Return true if Utf8Char contains one valid character.
729 bool isComplete() const
730 {
731 return count > 0 && count == charLength;
732 }
733 // Add the bytes in this UTF8 character in escaped form to data.
734 char *escapeBytes(char *data)
735 {
736 for (unsigned char i = 0; i < count; ++i) {
737 data = escapeByte(data, bytes[i]);
738 }
739 clear();
740 return data;
741 }
742 // Add the bytes of the UTF8 character to a buffer.
743 // Only call this if isComplete() returns true.
744 char *writeUtf8(char *data)
745 {
746 for (unsigned char i = 0; i < count; ++i) {
747 *data++ = bytes[i];
748 }
749 clear();
750 return data;
751 }
752 // Write the bytes in the UTF8 character literally, or, if the
753 // character is not complete, write the escaped bytes.
754 // This is useful to handle the state that remains after handling
755 // all bytes in a buffer.
756 char *write(char *data)
757 {
758 if (isComplete()) {
759 data = writeUtf8(data);
760 } else {
761 data = escapeBytes(data);
762 }
763 return data;
764 }
765};
766}
767
768QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type)
769{
770 const int len = aString.size();
771 if (len == 0) {
772 return aString;
773 }
774
775 QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of len*4
776 result.resize(len * 4); // Maximum 4x as long as source string due to \x<ab> escape sequences
777 const char *s = aString.constData();
778 int i = 0;
779 char *data = result.data();
780 char *start = data;
781
782 // Protect leading space
783 if (s[0] == ' ' && type != GroupString) {
784 *data++ = '\\';
785 *data++ = 's';
786 ++i;
787 }
788 Utf8Char utf8;
789
790 for (; i < len; ++i) {
791 switch (s[i]) {
792 default:
793 if (utf8.addByte(s[i])) {
794 break;
795 } else {
796 data = utf8.escapeBytes(data);
797 }
798 // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here
799 if (((unsigned char)s[i]) < 32) {
800 goto doEscape;
801 }
802 // GroupString and KeyString should be valid UTF-8, but ValueString
803 // can be a bytearray with non-UTF-8 bytes that should be escaped.
804 if (type == ValueString && ((unsigned char)s[i]) >= 127) {
805 goto doEscape;
806 }
807 *data++ = s[i];
808 break;
809 case '\n':
810 *data++ = '\\';
811 *data++ = 'n';
812 break;
813 case '\t':
814 *data++ = '\\';
815 *data++ = 't';
816 break;
817 case '\r':
818 *data++ = '\\';
819 *data++ = 'r';
820 break;
821 case '\\':
822 *data++ = '\\';
823 *data++ = '\\';
824 break;
825 case '=':
826 if (type != KeyString) {
827 *data++ = s[i];
828 break;
829 }
830 goto doEscape;
831 case '[':
832 case ']':
833 // Above chars are OK to put in *value* strings as plaintext
834 if (type == ValueString) {
835 *data++ = s[i];
836 break;
837 }
838 doEscape:
839 data = escapeByte(data, s[i]);
840 break;
841 }
842 if (utf8.isComplete()) {
843 data = utf8.writeUtf8(data);
844 }
845 }
846 data = utf8.write(data);
847 *data = 0;
848 result.resize(data - start);
849
850 // Protect trailing space
851 if (result.endsWith(' ') && type != GroupString) {
852 result.replace(result.length() - 1, 1, "\\s");
853 }
854
855 return result;
856}
857
858char KConfigIniBackend::charFromHex(const char *str, const QFile &file, int line)
859{
860 unsigned char ret = 0;
861 for (int i = 0; i < 2; i++) {
862 ret <<= 4;
863 quint8 c = quint8(str[i]);
864
865 if (c >= '0' && c <= '9') {
866 ret |= c - '0';
867 } else if (c >= 'a' && c <= 'f') {
868 ret |= c - 'a' + 0x0a;
869 } else if (c >= 'A' && c <= 'F') {
870 ret |= c - 'A' + 0x0a;
871 } else {
872 QByteArray e(str, 2);
873 e.prepend("\\x");
874 qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << "Invalid hex character " << c << " in \\x<nn>-type escape sequence \"" << e.constData()
875 << "\".";
876 return 'x';
877 }
878 }
879 return char(ret);
880}
881
882void KConfigIniBackend::printableToString(BufferFragment *aString, const QFile &file, int line)
883{
884 if (aString->isEmpty() || aString->indexOf('\\') == -1) {
885 return;
886 }
887 aString->trim();
888 int l = aString->length();
889 char *r = aString->data();
890 char *str = r;
891
892 for (int i = 0; i < l; i++, r++) {
893 if (str[i] != '\\') {
894 *r = str[i];
895 } else {
896 // Probable escape sequence
897 ++i;
898 if (i >= l) { // Line ends after backslash - stop.
899 *r = '\\';
900 break;
901 }
902
903 switch (str[i]) {
904 case 's':
905 *r = ' ';
906 break;
907 case 't':
908 *r = '\t';
909 break;
910 case 'n':
911 *r = '\n';
912 break;
913 case 'r':
914 *r = '\r';
915 break;
916 case '\\':
917 *r = '\\';
918 break;
919 case ';':
920 // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string
921 *r = '\\';
922 ++r;
923 *r = ';';
924 break;
925 case ',':
926 // not really an escape sequence, but allowed in .desktop files, don't strip '\,' from the string
927 *r = '\\';
928 ++r;
929 *r = ',';
930 break;
931 case 'x':
932 if (i + 2 < l) {
933 *r = charFromHex(str + i + 1, file, line);
934 i += 2;
935 } else {
936 *r = 'x';
937 i = l - 1;
938 }
939 break;
940 default:
941 *r = '\\';
942 qCWarning(KCONFIG_CORE_LOG).noquote() << warningProlog(file, line) << QStringLiteral("Invalid escape sequence: «\\%1»").arg(str[i]);
943 }
944 }
945 }
946 aString->truncate(r - aString->constData());
947}
948
949#include "moc_kconfigini_p.cpp"
AccessMode
Possible return values for accessMode().
Q_SCRIPTABLE Q_NOREPLY void start()
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 mid(qsizetype pos, qsizetype len) const const
QByteArray & replace(QByteArrayView before, QByteArrayView after)
void reserve(qsizetype size)
void resize(qsizetype newSize, char c)
qsizetype size() const const
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
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:27 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.