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 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 Fri Jul 26 2024 11:52:48 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.