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

KDE's Doxygen guidelines are available online.