KConfig

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

KDE's Doxygen guidelines are available online.