1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2006, 2007 Thomas Braxton <kde.braxton@gmail.com>
4 SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org>
5 SPDX-FileCopyrightText: 1997-1999 Matthias Kalle Dalheimer <kalle@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "kconfigini_p.h"
11
12#include "kconfig_core_log_settings.h"
13#include "kconfigdata_p.h"
14#include "kconfiginibackendreader_p.h"
15
16using namespace Qt::StringLiterals;
17
18KCONFIGCORE_EXPORT bool kde_kiosk_exception = false; // flag to disable kiosk restrictions
19
20namespace
21{
22QString warningProlog(const KConfigIniBackendAbstractDevice *device, int line)
23{
24 // %2 then %1 i.e. int before QString, so that the QString is last
25 // This avoids a wrong substitution if the fileName itself contains %1
26 return u"KConfigIni: In file %2, line %1:"_s.arg(a: line).arg(a: device->id());
27}
28} // anonymous namespace
29
30KConfigIniBackend::KConfigIniBackend(std::unique_ptr<KConfigIniBackendAbstractDevice> deviceInterface)
31 : mDeviceInterface(std::move(deviceInterface)) { };
32
33KConfigIniBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options)
34{
35 return parseConfig(locale: currentLocale, entryMap, options, merging: false);
36}
37
38// merging==true is the merging that happens at the beginning of writeConfig:
39// merge changes in the on-disk file with the changes in the KConfig object.
40KConfigIniBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray &currentLocale, KEntryMap &entryMap, ParseOptions options, bool merging)
41{
42 if (!mDeviceInterface->isDeviceReadable()) {
43 return ParseOk;
44 }
45
46 auto openResult = mDeviceInterface->open();
47 if (openResult.shouldHaveDevice && !openResult.device) {
48 return ParseOpenError;
49 }
50 auto file = std::move(openResult.device);
51 if (!file) {
52 return ParseOk;
53 }
54
55 if (file->size() == 0) {
56 return ParseOk;
57 }
58
59 QList<QString> immutableGroups;
60
61 bool fileOptionImmutable = false;
62 bool groupOptionImmutable = false;
63 bool groupSkip = false;
64
65 uint lineNo = 0;
66
67 const int langIdx = currentLocale.indexOf(ch: '_');
68 const QByteArray currentLanguage = langIdx >= 0 ? currentLocale.left(n: langIdx) : currentLocale;
69
70 QString currentGroup = u"<default>"_s;
71 bool bDefault = options & ParseDefaults;
72 bool allowExecutableValues = options & ParseExpansions;
73
74 const uint MAX_ERRORS = 100;
75 uint errorCount = 0;
76
77 const qint64 initialBufferSize = std::min(a: file->size(), b: qint64(128 * 1024) /* 128 KB */);
78 const uint maximumSizeWithoutNewLine = 1.5 * 1024 * 1024; // 1.5 MB
79 const uint defaultgroupNameBufferSize = 512; // should be a rather big fit for common group names
80
81 QByteArray groupNameBuffer; // reused allocated buffer to read group names, each processed into QString afterwards
82 groupNameBuffer.reserve(asize: defaultgroupNameBufferSize);
83
84#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
85 QByteArray buffer(initialBufferSize, Qt::Uninitialized);
86 while (!file->atEnd() && errorCount < MAX_ERRORS) {
87 auto res = file->readLineInto(&buffer, maximumSizeWithoutNewLine);
88 if (!res) {
89 qCWarning(KCONFIG_CORE_LOG) << "Couldn't find a single line in " << mDeviceInterface->id() << " after reading" << (maximumSizeWithoutNewLine)
90 << "bytes.";
91 }
92#else
93 QDataStream stream(file.get());
94 bool newLineFound = false;
95
96 QByteArray readBuffer(initialBufferSize, Qt::Uninitialized);
97 QByteArray buffer;
98 QByteArray leftOverBuffer;
99
100 while ((!stream.atEnd() || !leftOverBuffer.isEmpty()) && errorCount < MAX_ERRORS) {
101 buffer = leftOverBuffer;
102
103 int n = buffer.indexOf(ch: '\n');
104 if (n != -1) {
105 leftOverBuffer = buffer.sliced(pos: n + 1);
106 buffer = buffer.sliced(pos: 0, n);
107
108 } else if (!stream.atEnd()) {
109 while (!stream.atEnd()) {
110 int len = stream.readRawData(readBuffer.data(), len: initialBufferSize);
111 if (len == -1) {
112 qCWarning(KCONFIG_CORE_LOG) << "Couldn't read." << mDeviceInterface->id() << "after line" << lineNo;
113 return ParseOpenError;
114 }
115
116 QByteArrayView readBufferView(readBuffer.data(), len);
117 auto n = readBufferView.indexOf(ch: '\n');
118 if (n != -1) {
119 // found '\n' at position n
120 buffer += readBufferView.sliced(pos: 0, n);
121 leftOverBuffer = readBufferView.sliced(pos: n + 1).toByteArray();
122 newLineFound = true;
123 break;
124 } else {
125 // stream is atEnd or the \n was at the edge of the readBuffer
126 buffer += readBufferView;
127 leftOverBuffer = {};
128
129 if (!newLineFound && buffer.length() > maximumSizeWithoutNewLine) {
130 qCWarning(KCONFIG_CORE_LOG) << "Couldn't find a single line in " << mDeviceInterface->id() << " after reading"
131 << (maximumSizeWithoutNewLine) << "bytes.";
132 return ParseOpenError;
133 }
134 }
135 }
136
137 } else {
138 leftOverBuffer = {};
139 }
140#endif
141 QByteArrayView line = buffer;
142 line = line.trimmed();
143 ++lineNo;
144
145 // skip empty lines and lines beginning with '#'
146 if (line.isEmpty() || line.at(n: 0) == '#') {
147 continue;
148 }
149
150 if (line.at(n: 0) == '[') { // found a group
151 groupOptionImmutable = fileOptionImmutable;
152
153 groupNameBuffer.resize(size: 0); // drop content, but keep allocated capacity
154 int start = 1;
155 int end = 0;
156 do {
157 end = start;
158 for (;;) {
159 if (end == line.length()) {
160 qCWarning(KCONFIG_CORE_LOG) << warningProlog(device: mDeviceInterface.get(), line: lineNo) << "Invalid group header.";
161 // XXX maybe reset the current group here?
162 goto next_line;
163 }
164 if (line.at(n: end) == ']') {
165 break;
166 }
167 ++end;
168 }
169 /* clang-format off */
170 if (end + 1 == line.length()
171 && start + 2 == end
172 && line.at(n: start) == '$'
173 && line.at(n: start + 1) == 'i') { /* clang-format on */
174 if (groupNameBuffer.isEmpty()) {
175 fileOptionImmutable = !kde_kiosk_exception;
176 } else {
177 groupOptionImmutable = !kde_kiosk_exception;
178 }
179 } else {
180 if (!groupNameBuffer.isEmpty()) {
181 groupNameBuffer += '\x1d';
182 }
183 QByteArrayView namePart = line.mid(pos: start, n: end - start);
184 printableToString(aString&: namePart, device: mDeviceInterface.get(), line: lineNo);
185 groupNameBuffer.append(a: namePart);
186 }
187 } while ((start = end + 2) <= line.length() && line.at(n: end + 1) == '[');
188 currentGroup = QString::fromUtf8(ba: groupNameBuffer);
189
190 groupSkip = entryMap.getEntryOption(group: currentGroup, key: {}, flags: {}, option: KEntryMap::EntryImmutable);
191
192 if (groupSkip && !bDefault) {
193 continue;
194 }
195
196 if (groupOptionImmutable)
197 // Do not make the groups immutable until the entries from
198 // this file have been added.
199 {
200 immutableGroups.append(t: currentGroup);
201 }
202 } else {
203 if (groupSkip && !bDefault) {
204 continue; // skip entry
205 }
206
207 QByteArrayView aKey;
208 int eqpos = line.indexOf(ch: '=');
209 if (eqpos < 0) {
210 aKey = line;
211 line = {};
212 } else {
213 QByteArrayView temp = line.left(n: eqpos);
214 aKey = temp.trimmed();
215 line = line.mid(pos: eqpos + 1);
216 line = line.trimmed();
217 }
218 if (aKey.isEmpty()) {
219 qCWarning(KCONFIG_CORE_LOG) << warningProlog(device: mDeviceInterface.get(), line: lineNo) << "Invalid entry (empty key)";
220 continue;
221 }
222
223 KEntryMap::EntryOptions entryOptions = {};
224 if (groupOptionImmutable) {
225 entryOptions |= KEntryMap::EntryImmutable;
226 }
227
228 QByteArrayView locale;
229 int start;
230 while ((start = aKey.lastIndexOf(ch: '[')) >= 0) {
231 int end = aKey.indexOf(ch: ']', from: start);
232 if (end < 0) {
233 errorCount++;
234 qCWarning(KCONFIG_CORE_LOG) << warningProlog(device: mDeviceInterface.get(), line: lineNo) << "Invalid entry (missing ']')";
235 goto next_line;
236 } else if (end > start + 1 && aKey.at(n: start + 1) == '$') { // found option(s)
237 int i = start + 2;
238 while (i < end) {
239 switch (aKey.at(n: i)) {
240 case 'i':
241 if (!kde_kiosk_exception) {
242 entryOptions |= KEntryMap::EntryImmutable;
243 }
244 break;
245 case 'e':
246 if (allowExecutableValues) {
247 entryOptions |= KEntryMap::EntryExpansion;
248 }
249 break;
250 case 'd':
251 entryOptions |= KEntryMap::EntryDeleted;
252 aKey.truncate(n: start);
253 printableToString(aString&: aKey, device: mDeviceInterface.get(), line: lineNo);
254 entryMap.setEntry(group: currentGroup, key: aKey.toByteArray(), value: QByteArray(), options: entryOptions);
255 goto next_line;
256 default:
257 break;
258 }
259 ++i;
260 }
261 } else { // found a locale
262 if (!locale.isNull()) {
263 errorCount++;
264 qCWarning(KCONFIG_CORE_LOG) << warningProlog(device: mDeviceInterface.get(), line: lineNo) << "Invalid entry (second locale!?)";
265 goto next_line;
266 }
267
268 locale = aKey.mid(pos: start + 1, n: end - start - 1);
269 }
270 aKey.truncate(n: start);
271 }
272 if (eqpos < 0) { // Do this here after [$d] was checked
273 errorCount++;
274 qCWarning(KCONFIG_CORE_LOG) << warningProlog(device: mDeviceInterface.get(), line: lineNo) << "Invalid entry (missing '=')";
275 continue;
276 }
277 printableToString(aString&: aKey, device: mDeviceInterface.get(), line: lineNo);
278 if (!locale.isEmpty()) {
279 if (locale != currentLocale && locale != currentLanguage) {
280 // backward compatibility. C == en_US
281 if (locale.at(n: 0) != 'C' || currentLocale != "en_US") {
282 if (merging) {
283 entryOptions |= KEntryMap::EntryRawKey;
284 } else {
285 goto next_line; // skip this entry if we're not merging
286 }
287 }
288 }
289 }
290
291 if (options & ParseGlobal) {
292 entryOptions |= KEntryMap::EntryGlobal;
293 }
294 if (bDefault) {
295 entryOptions |= KEntryMap::EntryDefault;
296 }
297 if (!locale.isNull()) {
298 entryOptions |= KEntryMap::EntryLocalized;
299 if (locale.indexOf(ch: '_') != -1) {
300 entryOptions |= KEntryMap::EntryLocalizedCountry;
301 }
302 }
303 if (!printableToString(aString&: line, device: mDeviceInterface.get(), line: lineNo)) {
304 errorCount++;
305 }
306 if (entryOptions & KEntryMap::EntryRawKey) {
307 QByteArray rawKey;
308 rawKey.reserve(asize: aKey.length() + locale.length() + 2);
309 rawKey.append(a: aKey);
310 rawKey.append(c: '[').append(a: locale).append(c: ']');
311 entryMap.setEntry(group: currentGroup, key: rawKey, value: line.toByteArray(), options: entryOptions);
312 } else {
313 entryMap.setEntry(group: currentGroup, key: aKey.toByteArray(), value: line.toByteArray(), options: entryOptions);
314 }
315 }
316 next_line:
317 continue;
318 }
319
320 if (errorCount > MAX_ERRORS) {
321 qCWarning(KCONFIG_CORE_LOG) << "Too many errors in file" << mDeviceInterface->id();
322 return ParseOpenError;
323 }
324
325 // now make sure immutable groups are marked immutable
326 for (const QString &group : std::as_const(t&: immutableGroups)) {
327 entryMap.setEntry(group, key: QByteArray(), value: QByteArray(), options: KEntryMap::EntryImmutable);
328 }
329
330 return fileOptionImmutable ? ParseImmutable : ParseOk;
331}
332
333void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map, bool defaultGroup, bool primaryGroup, bool &firstEntry)
334{
335 QString currentGroup;
336 bool groupIsImmutable = false;
337 for (const auto &[key, entry] : map) {
338 // Either process the default group or all others
339 if ((key.mGroup != u"<default>"_s) == defaultGroup) {
340 continue; // skip
341 }
342 // Either process the primary group or all others
343 if ((mPrimaryGroup.isNull() || key.mGroup != mPrimaryGroup) == primaryGroup) {
344 continue; // skip
345 }
346
347 // the only thing we care about groups is, is it immutable?
348 if (key.mKey.isNull()) {
349 groupIsImmutable = entry.bImmutable;
350 continue; // skip
351 }
352
353 const KEntry &currentEntry = entry;
354 if (!defaultGroup && currentGroup != key.mGroup) {
355 if (!firstEntry) {
356 file.putChar(c: '\n');
357 }
358 currentGroup = key.mGroup;
359 for (int start = 0, end;; start = end + 1) {
360 file.putChar(c: '[');
361 end = currentGroup.indexOf(ch: QLatin1Char('\x1d'), from: start);
362 if (end < 0) {
363 int cgl = currentGroup.length();
364 // Start has to be smaller than currentGroup length, or otherwise currentGroup.at()
365 // will assert: https://doc.qt.io/qt-6/qstring.html#at
366 if (cgl > start && cgl - start <= 10 && currentGroup.at(i: start) == QLatin1Char('$')) {
367 for (int i = start + 1; i < cgl; i++) {
368 const QChar c = currentGroup.at(i);
369 if (c < QLatin1Char('a') || c > QLatin1Char('z')) {
370 goto nope;
371 }
372 }
373 file.write(data: "\\x24");
374 ++start;
375 }
376 nope:
377 // TODO: make stringToPrintable also process QString, to save the conversion here and below
378 file.write(data: stringToPrintable(aString: QStringView(currentGroup).mid(pos: start).toUtf8(), type: GroupString));
379 file.putChar(c: ']');
380 if (groupIsImmutable) {
381 file.write(data: "[$i]", len: 4);
382 }
383 file.putChar(c: '\n');
384 break;
385 } else {
386 file.write(data: stringToPrintable(aString: QStringView(currentGroup).mid(pos: start, n: end - start).toUtf8(), type: GroupString));
387 file.putChar(c: ']');
388 }
389 }
390 }
391
392 firstEntry = false;
393 // it is data for a group
394
395 if (key.bRaw) { // unprocessed key with attached locale from merge
396 file.write(data: key.mKey);
397 } else {
398 file.write(data: stringToPrintable(aString: key.mKey, type: KeyString)); // Key
399 if (key.bLocal && locale != "C") { // 'C' locale == untranslated
400 file.putChar(c: '[');
401 file.write(data: locale); // locale tag
402 file.putChar(c: ']');
403 }
404 }
405 if (currentEntry.bDeleted) {
406 if (currentEntry.bImmutable) {
407 file.write(data: "[$di]", len: 5); // Deleted + immutable
408 } else {
409 file.write(data: "[$d]", len: 4); // Deleted
410 }
411 } else {
412 if (currentEntry.bImmutable || currentEntry.bExpand) {
413 file.write(data: "[$", len: 2);
414 if (currentEntry.bImmutable) {
415 file.putChar(c: 'i');
416 }
417 if (currentEntry.bExpand) {
418 file.putChar(c: 'e');
419 }
420 file.putChar(c: ']');
421 }
422 file.putChar(c: '=');
423 file.write(data: stringToPrintable(aString: currentEntry.mValue, type: ValueString));
424 }
425 file.putChar(c: '\n');
426 }
427}
428
429void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map)
430{
431 bool firstEntry = true;
432
433 // write default group
434 writeEntries(locale, file, map, defaultGroup: true, primaryGroup: false, firstEntry);
435
436 if (!mPrimaryGroup.isNull()) {
437 // write the primary group - it needs to be written before all other groups
438 writeEntries(locale, file, map, defaultGroup: false, primaryGroup: true, firstEntry);
439 }
440
441 // write all other groups
442 writeEntries(locale, file, map, defaultGroup: false, primaryGroup: false, firstEntry);
443}
444
445bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options)
446{
447 Q_ASSERT(mDeviceInterface->isDeviceReadable());
448
449 KEntryMap writeMap;
450 const bool bGlobal = options & WriteGlobal;
451
452 // First, reparse the file on disk, to merge our changes with the ones done by other apps
453 // Store the result into writeMap.
454 {
455 ParseOptions opts = ParseExpansions;
456 if (bGlobal) {
457 opts |= ParseGlobal;
458 }
459 ParseInfo info = parseConfig(currentLocale: locale, entryMap&: writeMap, options: opts, merging: true);
460 if (info != ParseOk) { // either there was an error or the file became immutable
461 return false;
462 }
463 }
464
465 for (auto &[key, entry] : entryMap) {
466 if (!key.mKey.isEmpty() && !entry.bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too.
467 continue;
468 }
469
470 // only write entries that have the same "globality" as the file
471 if (entry.bGlobal == bGlobal) {
472 if (entry.bReverted && entry.bOverridesGlobal) {
473 entry.bDeleted = true;
474 writeMap[key] = entry;
475 } else if (entry.bReverted) {
476 writeMap.erase(x: key);
477 } else if (!entry.bDeleted) {
478 writeMap[key] = entry;
479 } else {
480 KEntryKey defaultKey = key;
481 defaultKey.bDefault = true;
482 if (entryMap.find(x: defaultKey) == entryMap.end() && !entry.bOverridesGlobal) {
483 writeMap.erase(x: key); // remove the deleted entry if there is no default
484 // qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal;
485 } else {
486 writeMap[key] = entry; // otherwise write an explicitly deleted entry
487 // qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal;
488 }
489 }
490 entry.bDirty = false;
491 }
492 }
493
494 return mDeviceInterface->writeToDevice(write: [this, &locale, &writeMap](auto &device) {
495 writeEntries(locale, device, writeMap);
496 });
497}
498
499bool KConfigIniBackend::isWritable() const
500{
501 return mDeviceInterface->canWriteToDevice();
502}
503
504QString KConfigIniBackend::nonWritableErrorMessage() const
505{
506 return tr(sourceText: "Configuration file \"%1\" not writable.\n").arg(a: mDeviceInterface->id());
507}
508
509void KConfigIniBackend::createEnclosing()
510{
511 mDeviceInterface->createEnclosingEntity();
512}
513
514KConfigBase::AccessMode KConfigIniBackend::accessMode() const
515{
516 if (!mDeviceInterface->isDeviceReadable()) {
517 return KConfigBase::NoAccess;
518 }
519
520 if (isWritable()) {
521 return KConfigBase::ReadWrite;
522 }
523
524 return KConfigBase::ReadOnly;
525}
526
527bool KConfigIniBackend::lock()
528{
529 Q_ASSERT(mDeviceInterface->isDeviceReadable());
530
531 // Default staleLockTime is 30 seconds. Set it lower since KConfig is not expected to hold the
532 // lock for long. The tryLockTimeout is set to staleLockTime*2+buffer, to cover the case when
533 // the file modification date is in the future, and the fallback to prevent blocking forever.
534 constexpr std::chrono::milliseconds tryLockTimeout = std::chrono::seconds{45};
535
536 lockFile = mDeviceInterface->lockFile();
537
538 if (!lockFile->tryLock(timeout: tryLockTimeout)) {
539 qCWarning(KCONFIG_CORE_LOG) << "Failed to lock file" << lockFile->fileName() << "with error" << int(lockFile->error());
540 }
541
542 return lockFile->isLocked();
543}
544
545void KConfigIniBackend::unlock()
546{
547 lockFile.reset();
548}
549
550bool KConfigIniBackend::isLocked() const
551{
552 return lockFile && lockFile->isLocked();
553}
554
555namespace
556{
557// serialize an escaped byte at the end of @param data
558// @param data should have room for 4 bytes
559char *escapeByte(char *data, unsigned char s)
560{
561 static const char nibbleLookup[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
562 *data++ = '\\';
563 *data++ = 'x';
564 *data++ = nibbleLookup[s >> 4];
565 *data++ = nibbleLookup[s & 0x0f];
566 return data;
567}
568
569// Struct that represents a multi-byte UTF-8 character.
570// This struct is used to keep track of bytes that seem to be valid
571// UTF-8.
572struct Utf8Char {
573public:
574 unsigned char bytes[4];
575 unsigned char count;
576 unsigned char charLength;
577
578 Utf8Char()
579 {
580 clear();
581 charLength = 0;
582 }
583 void clear()
584 {
585 count = 0;
586 }
587 // Add a byte to the UTF8 character.
588 // When an additional byte leads to an invalid character, return false.
589 bool addByte(unsigned char b)
590 {
591 if (count == 0) {
592 if (b > 0xc1 && (b & 0xe0) == 0xc0) {
593 charLength = 2;
594 } else if ((b & 0xf0) == 0xe0) {
595 charLength = 3;
596 } else if (b < 0xf5 && (b & 0xf8) == 0xf0) {
597 charLength = 4;
598 } else {
599 return false;
600 }
601 bytes[0] = b;
602 count = 1;
603 } else if (count < 4 && (b & 0xc0) == 0x80) {
604 if (count == 1) {
605 if (charLength == 3 && bytes[0] == 0xe0 && b < 0xa0) {
606 return false; // overlong 3 byte sequence
607 }
608 if (charLength == 4) {
609 if (bytes[0] == 0xf0 && b < 0x90) {
610 return false; // overlong 4 byte sequence
611 }
612 if (bytes[0] == 0xf4 && b > 0x8f) {
613 return false; // Unicode value larger than U+10FFFF
614 }
615 }
616 }
617 bytes[count++] = b;
618 } else {
619 return false;
620 }
621 return true;
622 }
623 // Return true if Utf8Char contains one valid character.
624 bool isComplete() const
625 {
626 return count > 0 && count == charLength;
627 }
628 // Add the bytes in this UTF8 character in escaped form to data.
629 char *escapeBytes(char *data)
630 {
631 for (unsigned char i = 0; i < count; ++i) {
632 data = escapeByte(data, s: bytes[i]);
633 }
634 clear();
635 return data;
636 }
637 // Add the bytes of the UTF8 character to a buffer.
638 // Only call this if isComplete() returns true.
639 char *writeUtf8(char *data)
640 {
641 for (unsigned char i = 0; i < count; ++i) {
642 *data++ = bytes[i];
643 }
644 clear();
645 return data;
646 }
647 // Write the bytes in the UTF8 character literally, or, if the
648 // character is not complete, write the escaped bytes.
649 // This is useful to handle the state that remains after handling
650 // all bytes in a buffer.
651 char *write(char *data)
652 {
653 if (isComplete()) {
654 data = writeUtf8(data);
655 } else {
656 data = escapeBytes(data);
657 }
658 return data;
659 }
660};
661}
662
663QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type)
664{
665 const int len = aString.size();
666 if (len == 0) {
667 return aString;
668 }
669
670 QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of len*4
671 result.resize(size: len * 4); // Maximum 4x as long as source string due to \x<ab> escape sequences
672 const char *s = aString.constData();
673 int i = 0;
674 char *data = result.data();
675 char *start = data;
676
677 // Protect leading space
678 if (s[0] == ' ' && type != GroupString) {
679 *data++ = '\\';
680 *data++ = 's';
681 ++i;
682 }
683 Utf8Char utf8;
684
685 for (; i < len; ++i) {
686 switch (s[i]) {
687 default:
688 if (utf8.addByte(b: s[i])) {
689 break;
690 } else {
691 data = utf8.escapeBytes(data);
692 }
693 // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here
694 if (((unsigned char)s[i]) < 32) {
695 goto doEscape;
696 }
697 // GroupString and KeyString should be valid UTF-8, but ValueString
698 // can be a bytearray with non-UTF-8 bytes that should be escaped.
699 if (type == ValueString && ((unsigned char)s[i]) >= 127) {
700 goto doEscape;
701 }
702 *data++ = s[i];
703 break;
704 case '\n':
705 *data++ = '\\';
706 *data++ = 'n';
707 break;
708 case '\t':
709 *data++ = '\\';
710 *data++ = 't';
711 break;
712 case '\r':
713 *data++ = '\\';
714 *data++ = 'r';
715 break;
716 case '\\':
717 *data++ = '\\';
718 *data++ = '\\';
719 break;
720 case '=':
721 if (type != KeyString) {
722 *data++ = s[i];
723 break;
724 }
725 goto doEscape;
726 case '[':
727 case ']':
728 // Above chars are OK to put in *value* strings as plaintext
729 if (type == ValueString) {
730 *data++ = s[i];
731 break;
732 }
733 doEscape:
734 data = escapeByte(data, s: s[i]);
735 break;
736 }
737 if (utf8.isComplete()) {
738 data = utf8.writeUtf8(data);
739 }
740 }
741 data = utf8.write(data);
742 *data = 0;
743 result.resize(size: data - start);
744
745 // Protect trailing space
746 if (result.endsWith(c: ' ') && type != GroupString) {
747 result.replace(index: result.length() - 1, len: 1, s: "\\s");
748 }
749
750 return result;
751}
752
753char KConfigIniBackend::charFromHex(const char *str, const KConfigIniBackendAbstractDevice *device, int line)
754{
755 unsigned char ret = 0;
756 for (int i = 0; i < 2; i++) {
757 ret <<= 4;
758 quint8 c = quint8(str[i]);
759
760 if (c >= '0' && c <= '9') {
761 ret |= c - '0';
762 } else if (c >= 'a' && c <= 'f') {
763 ret |= c - 'a' + 0x0a;
764 } else if (c >= 'A' && c <= 'F') {
765 ret |= c - 'A' + 0x0a;
766 } else {
767 QByteArray e(str, 2);
768 e.prepend(s: "\\x");
769 qCWarning(KCONFIG_CORE_LOG) << warningProlog(device, line) << "Invalid hex character " << c << " in \\x<nn>-type escape sequence \""
770 << e.constData() << "\".";
771 return 'x';
772 }
773 }
774 return char(ret);
775}
776
777bool KConfigIniBackend::printableToString(QByteArrayView &aString, const KConfigIniBackendAbstractDevice *device, int line)
778{
779 if (aString.isEmpty() || aString.indexOf(ch: '\\') == -1) {
780 return true;
781 }
782 aString = aString.trimmed();
783 int l = aString.size();
784 char *r = const_cast<char *>(aString.data());
785 char *str = r;
786
787 for (int i = 0; i < l; i++, r++) {
788 if (str[i] != '\\') {
789 *r = str[i];
790 } else {
791 // Probable escape sequence
792 ++i;
793 if (i >= l) { // Line ends after backslash - stop.
794 *r = '\\';
795 break;
796 }
797
798 switch (str[i]) {
799 case 's':
800 *r = ' ';
801 break;
802 case 't':
803 *r = '\t';
804 break;
805 case 'n':
806 *r = '\n';
807 break;
808 case 'r':
809 *r = '\r';
810 break;
811 case '\\':
812 *r = '\\';
813 break;
814 case ';':
815 // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string
816 *r = '\\';
817 ++r;
818 *r = ';';
819 break;
820 case ',':
821 // not really an escape sequence, but allowed in .desktop files, don't strip '\,' from the string
822 *r = '\\';
823 ++r;
824 *r = ',';
825 break;
826 case 'x':
827 if (i + 2 < l) {
828 *r = charFromHex(str: str + i + 1, device, line);
829 i += 2;
830 } else {
831 *r = 'x';
832 i = l - 1;
833 }
834 break;
835 default:
836 *r = '\\';
837 qCWarning(KCONFIG_CORE_LOG).noquote() << warningProlog(device, line) << QStringLiteral("Invalid escape sequence: «\\%1»").arg(a: str[i]);
838 return false;
839 }
840 }
841 }
842 aString.truncate(n: r - aString.constData());
843 return true;
844}
845
846void KConfigIniBackend::setPrimaryGroup(const QString &group)
847{
848 mPrimaryGroup = group;
849}
850
851bool KConfigIniBackend::hasOpenableDeviceInterface() const
852{
853 return mDeviceInterface->isDeviceReadable();
854}
855
856QString KConfigIniBackend::backingDevicePath() const
857{
858 if (dynamic_cast<KConfigIniBackendPathDevice *>(mDeviceInterface.get())) {
859 return mDeviceInterface->id();
860 }
861 return {};
862}
863
864#include "moc_kconfigini_p.cpp"
865

source code of kconfig/src/core/kconfigini.cpp