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

KDE's Doxygen guidelines are available online.