diff --git a/src/core/kconfigini.cpp b/src/core/kconfigini.cpp index 6ae6345..85ece83 100644 --- a/src/core/kconfigini.cpp +++ b/src/core/kconfigini.cpp @@ -1,819 +1,818 @@ /* This file is part of the KDE libraries Copyright (c) 2006, 2007 Thomas Braxton Copyright (c) 1999 Preston Brown Copyright (C) 1997-1999 Matthias Kalle Dalheimer (kalle@kde.org) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kconfigini_p.h" #include "kconfig.h" #include "kconfigbackend.h" #include "bufferfragment_p.h" #include "kconfigdata.h" #include #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include // getuid, close #endif #include // uid_t #include // open KCONFIGCORE_EXPORT bool kde_kiosk_exception = false; // flag to disable kiosk restrictions static QByteArray lookup(const KConfigIniBackend::BufferFragment &fragment, QHash *cache) { auto it = cache->constFind(fragment); if (it != cache->constEnd()) { return it.value(); } return cache->insert(fragment, fragment.toByteArray()).value(); } QString KConfigIniBackend::warningProlog(const QFile &file, int line) { return QStringLiteral("KConfigIni: In file %2, line %1: ") .arg(line).arg(file.fileName()); } KConfigIniBackend::KConfigIniBackend() : KConfigBackend(), lockFile(nullptr) { } KConfigIniBackend::~KConfigIniBackend() { } KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray ¤tLocale, KEntryMap &entryMap, ParseOptions options) { return parseConfig(currentLocale, entryMap, options, false); } // merging==true is the merging that happens at the beginning of writeConfig: // merge changes in the on-disk file with the changes in the KConfig object. KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray ¤tLocale, KEntryMap &entryMap, ParseOptions options, bool merging) { if (filePath().isEmpty() || !QFile::exists(filePath())) { return ParseOk; } const QByteArray currentLanguage = currentLocale.split('_').first(); bool bDefault = options & ParseDefaults; bool allowExecutableValues = options & ParseExpansions; QByteArray currentGroup(""); QFile file(filePath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return ParseOpenError; } QList immutableGroups; bool fileOptionImmutable = false; bool groupOptionImmutable = false; bool groupSkip = false; int lineNo = 0; // on systems using \r\n as end of line, \r will be taken care of by // trim() below QByteArray buffer = file.readAll(); BufferFragment contents(buffer.data(), buffer.size()); unsigned int len = contents.length(); unsigned int startOfLine = 0; // Reduce memory overhead by making use of implicit sharing // This assumes that config files contain only a small amount of // different fragments which are repeated often. // This is often the case, especially sub groups will all have // the same list of keys and similar values as well. QHash cache; cache.reserve(4096); while (startOfLine < len) { BufferFragment line = contents.split('\n', &startOfLine); line.trim(); lineNo++; // skip empty lines and lines beginning with '#' if (line.isEmpty() || line.at(0) == '#') { continue; } if (line.at(0) == '[') { // found a group groupOptionImmutable = fileOptionImmutable; QByteArray newGroup; int start = 1, end; do { end = start; for (;;) { if (end == line.length()) { qWarning() << warningProlog(file, lineNo) << "Invalid group header."; // XXX maybe reset the current group here? goto next_line; } if (line.at(end) == ']') { break; } end++; } if (end + 1 == line.length() && start + 2 == end && line.at(start) == '$' && line.at(start + 1) == 'i') { if (newGroup.isEmpty()) { fileOptionImmutable = !kde_kiosk_exception; } else { groupOptionImmutable = !kde_kiosk_exception; } } else { if (!newGroup.isEmpty()) { newGroup += '\x1d'; } BufferFragment namePart = line.mid(start, end - start); printableToString(&namePart, file, lineNo); newGroup += namePart.toByteArray(); } } while ((start = end + 2) <= line.length() && line.at(end + 1) == '['); currentGroup = newGroup; groupSkip = entryMap.getEntryOption(currentGroup, nullptr, nullptr, KEntryMap::EntryImmutable); if (groupSkip && !bDefault) { continue; } if (groupOptionImmutable) // Do not make the groups immutable until the entries from // this file have been added. { immutableGroups.append(currentGroup); } } else { if (groupSkip && !bDefault) { continue; // skip entry } BufferFragment aKey; int eqpos = line.indexOf('='); if (eqpos < 0) { aKey = line; line.clear(); } else { BufferFragment temp = line.left(eqpos); temp.trim(); aKey = temp; line.truncateLeft(eqpos + 1); } if (aKey.isEmpty()) { qWarning() << warningProlog(file, lineNo) << "Invalid entry (empty key)"; continue; } KEntryMap::EntryOptions entryOptions = nullptr; if (groupOptionImmutable) { entryOptions |= KEntryMap::EntryImmutable; } BufferFragment locale; int start; while ((start = aKey.lastIndexOf('[')) >= 0) { int end = aKey.indexOf(']', start); if (end < 0) { qWarning() << warningProlog(file, lineNo) << "Invalid entry (missing ']')"; goto next_line; } else if (end > start + 1 && aKey.at(start + 1) == '$') { // found option(s) int i = start + 2; while (i < end) { switch (aKey.at(i)) { case 'i': if (!kde_kiosk_exception) { entryOptions |= KEntryMap::EntryImmutable; } break; case 'e': if (allowExecutableValues) { entryOptions |= KEntryMap::EntryExpansion; } break; case 'd': entryOptions |= KEntryMap::EntryDeleted; aKey = aKey.left(start); printableToString(&aKey, file, lineNo); entryMap.setEntry(currentGroup, aKey.toByteArray(), QByteArray(), entryOptions); goto next_line; default: break; } i++; } } else { // found a locale if (!locale.isNull()) { qWarning() << warningProlog(file, lineNo) << "Invalid entry (second locale!?)"; goto next_line; } locale = aKey.mid(start + 1, end - start - 1); } aKey.truncate(start); } if (eqpos < 0) { // Do this here after [$d] was checked qWarning() << warningProlog(file, lineNo) << "Invalid entry (missing '=')"; continue; } printableToString(&aKey, file, lineNo); if (!locale.isEmpty()) { if (locale != currentLocale && locale != currentLanguage) { // backward compatibility. C == en_US if (locale.at(0) != 'C' || currentLocale != "en_US") { if (merging) { entryOptions |= KEntryMap::EntryRawKey; } else { goto next_line; // skip this entry if we're not merging } } } } if (!(entryOptions & KEntryMap::EntryRawKey)) { printableToString(&aKey, file, lineNo); } if (options & ParseGlobal) { entryOptions |= KEntryMap::EntryGlobal; } if (bDefault) { entryOptions |= KEntryMap::EntryDefault; } if (!locale.isNull()) { entryOptions |= KEntryMap::EntryLocalized; if (locale.indexOf('_') != -1) { entryOptions |= KEntryMap::EntryLocalizedCountry; } } printableToString(&line, file, lineNo); if (entryOptions & KEntryMap::EntryRawKey) { QByteArray rawKey; rawKey.reserve(aKey.length() + locale.length() + 2); rawKey.append(aKey.toVolatileByteArray()); rawKey.append('[').append(locale.toVolatileByteArray()).append(']'); entryMap.setEntry(currentGroup, rawKey, lookup(line, &cache), entryOptions); } else { entryMap.setEntry(currentGroup, lookup(aKey, &cache), lookup(line, &cache), entryOptions); } } next_line: continue; } // now make sure immutable groups are marked immutable Q_FOREACH (const QByteArray &group, immutableGroups) { entryMap.setEntry(group, QByteArray(), QByteArray(), KEntryMap::EntryImmutable); } return fileOptionImmutable ? ParseImmutable : ParseOk; } void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map, bool defaultGroup, bool &firstEntry) { QByteArray currentGroup; bool groupIsImmutable = false; const KEntryMapConstIterator end = map.constEnd(); for (KEntryMapConstIterator it = map.constBegin(); it != end; ++it) { const KEntryKey &key = it.key(); // Either process the default group or all others if ((key.mGroup != "") == defaultGroup) { continue; // skip } // the only thing we care about groups is, is it immutable? if (key.mKey.isNull()) { groupIsImmutable = it->bImmutable; continue; // skip } const KEntry ¤tEntry = *it; if (!defaultGroup && currentGroup != key.mGroup) { if (!firstEntry) { file.putChar('\n'); } currentGroup = key.mGroup; for (int start = 0, end;; start = end + 1) { file.putChar('['); end = currentGroup.indexOf('\x1d', start); if (end < 0) { int cgl = currentGroup.length(); if (currentGroup.at(start) == '$' && cgl - start <= 10) { for (int i = start + 1; i < cgl; i++) { char c = currentGroup.at(i); if (c < 'a' || c > 'z') { goto nope; } } file.write("\\x24"); start++; } nope: file.write(stringToPrintable(currentGroup.mid(start), GroupString)); file.putChar(']'); if (groupIsImmutable) { file.write("[$i]", 4); } file.putChar('\n'); break; } else { file.write(stringToPrintable(currentGroup.mid(start, end - start), GroupString)); file.putChar(']'); } } } firstEntry = false; // it is data for a group if (key.bRaw) { // unprocessed key with attached locale from merge file.write(key.mKey); } else { file.write(stringToPrintable(key.mKey, KeyString)); // Key if (key.bLocal && locale != "C") { // 'C' locale == untranslated file.putChar('['); file.write(locale); // locale tag file.putChar(']'); } } if (currentEntry.bDeleted) { if (currentEntry.bImmutable) { file.write("[$di]", 5); // Deleted + immutable } else { file.write("[$d]", 4); // Deleted } } else { if (currentEntry.bImmutable || currentEntry.bExpand) { file.write("[$", 2); if (currentEntry.bImmutable) { file.putChar('i'); } if (currentEntry.bExpand) { file.putChar('e'); } file.putChar(']'); } file.putChar('='); file.write(stringToPrintable(currentEntry.mValue, ValueString)); } file.putChar('\n'); } } void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map) { bool firstEntry = true; // write default group writeEntries(locale, file, map, true, firstEntry); // write all other groups writeEntries(locale, file, map, false, firstEntry); } bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options) { Q_ASSERT(!filePath().isEmpty()); KEntryMap writeMap; const bool bGlobal = options & WriteGlobal; // First, reparse the file on disk, to merge our changes with the ones done by other apps // Store the result into writeMap. { ParseOptions opts = ParseExpansions; if (bGlobal) { opts |= ParseGlobal; } ParseInfo info = parseConfig(locale, writeMap, opts, true); if (info != ParseOk) { // either there was an error or the file became immutable return false; } } const KEntryMapIterator end = entryMap.end(); for (KEntryMapIterator it = entryMap.begin(); it != end; ++it) { if (!it.key().mKey.isEmpty() && !it->bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too. continue; } const KEntryKey &key = it.key(); // only write entries that have the same "globality" as the file if (it->bGlobal == bGlobal) { if (it->bReverted) { writeMap.remove(key); } else if (!it->bDeleted) { writeMap[key] = *it; } else { KEntryKey defaultKey = key; defaultKey.bDefault = true; if (!entryMap.contains(defaultKey)) { writeMap.remove(key); // remove the deleted entry if there is no default //qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal; } else { writeMap[key] = *it; // otherwise write an explicitly deleted entry //qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal; } } it->bDirty = false; } } // now writeMap should contain only entries to be written // so write it out to disk // check if file exists QFile::Permissions fileMode = QFile::ReadUser | QFile::WriteUser; bool createNew = true; QFileInfo fi(filePath()); if (fi.exists()) { #ifdef Q_OS_WIN //TODO: getuid does not exist on windows, use GetSecurityInfo and GetTokenInformation instead createNew = false; #else if (fi.ownerId() == ::getuid()) { // Preserve file mode if file exists and is owned by user. fileMode = fi.permissions(); } else { // File is not owned by user: // Don't create new file but write to existing file instead. createNew = false; } #endif } if (createNew) { QSaveFile file(filePath()); if (!file.open(QIODevice::WriteOnly)) { return false; } file.setTextModeEnabled(true); // to get eol translation writeEntries(locale, file, writeMap); if (!file.size() && (fileMode == (QFile::ReadUser | QFile::WriteUser))) { // File is empty and doesn't have special permissions: delete it. file.cancelWriting(); if (fi.exists()) { // also remove the old file in case it existed. this can happen // when we delete all the entries in an existing config file. // if we don't do this, then deletions and revertToDefault's // will mysteriously fail QFile::remove(filePath()); } } else { // Normal case: Close the file if (file.commit()) { QFile::setPermissions(filePath(), fileMode); return true; } // Couldn't write. Disk full? qWarning() << "Couldn't write" << filePath() << ". Disk full?"; return false; } } else { // Open existing file. *DON'T* create it if it suddenly does not exist! #ifdef Q_OS_UNIX int fd = QT_OPEN(QFile::encodeName(filePath()).constData(), O_WRONLY | O_TRUNC); if (fd < 0) { return false; } FILE *fp = ::fdopen(fd, "w"); if (!fp) { QT_CLOSE(fd); return false; } QFile f; if (!f.open(fp, QIODevice::WriteOnly)) { fclose(fp); return false; } writeEntries(locale, f, writeMap); f.close(); fclose(fp); #else QFile f(filePath()); // XXX This is broken - it DOES create the file if it is suddenly gone. if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } f.setTextModeEnabled(true); writeEntries(locale, f, writeMap); #endif } return true; } bool KConfigIniBackend::isWritable() const { const QString filePath = this->filePath(); if (!filePath.isEmpty()) { QFileInfo file(filePath); if (!file.exists()) { // If the file does not exist, check if the deepest // existing dir is writable. QFileInfo dir(file.absolutePath()); while (!dir.exists()) { QString parent = dir.absolutePath(); // Go up. Can't use cdUp() on non-existing dirs. if (parent == dir.filePath()) { // no parent return false; } dir.setFile(parent); } return dir.isDir() && dir.isWritable(); } else { return file.isWritable(); } } return false; } QString KConfigIniBackend::nonWritableErrorMessage() const { return tr("Configuration file \"%1\" not writable.\n").arg(filePath()); } void KConfigIniBackend::createEnclosing() { const QString file = filePath(); if (file.isEmpty()) { return; // nothing to do } // Create the containing dir, maybe it wasn't there QDir dir; dir.mkpath(QFileInfo(file).absolutePath()); } void KConfigIniBackend::setFilePath(const QString &file) { if (file.isEmpty()) { return; } Q_ASSERT(QDir::isAbsolutePath(file)); const QFileInfo info(file); if (info.exists()) { setLocalFilePath(info.canonicalFilePath()); setLastModified(info.lastModified()); setSize(info.size()); } else { setLocalFilePath(file); setSize(0); QDateTime dummy; dummy.setTime_t(0); setLastModified(dummy); } } KConfigBase::AccessMode KConfigIniBackend::accessMode() const { if (filePath().isEmpty()) { return KConfigBase::NoAccess; } if (isWritable()) { return KConfigBase::ReadWrite; } return KConfigBase::ReadOnly; } bool KConfigIniBackend::lock() { Q_ASSERT(!filePath().isEmpty()); if (!lockFile) { lockFile = new QLockFile(filePath() + QLatin1String(".lock")); } lockFile->lock(); return lockFile->isLocked(); } void KConfigIniBackend::unlock() { lockFile->unlock(); delete lockFile; lockFile = nullptr; } bool KConfigIniBackend::isLocked() const { return lockFile && lockFile->isLocked(); } QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type) { static const char nibbleLookup[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; if (aString.isEmpty()) { return aString; } const int l = aString.length(); QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of l*4 result.resize(l * 4); // Maximum 4x as long as source string due to \x escape sequences const char *s = aString.constData(); int i = 0; char *data = result.data(); char *start = data; // Protect leading space if (s[0] == ' ' && type != GroupString) { *data++ = '\\'; *data++ = 's'; i++; } for (; i < l; ++i/*, r++*/) { switch (s[i]) { default: // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here if (((unsigned char)s[i]) < 32) { goto doEscape; } *data++ = s[i]; break; case '\n': *data++ = '\\'; *data++ = 'n'; break; case '\t': *data++ = '\\'; *data++ = 't'; break; case '\r': *data++ = '\\'; *data++ = 'r'; break; case '\\': *data++ = '\\'; *data++ = '\\'; break; case '=': if (type != KeyString) { *data++ = s[i]; break; } goto doEscape; case '[': case ']': // Above chars are OK to put in *value* strings as plaintext if (type == ValueString) { *data++ = s[i]; break; } doEscape: *data++ = '\\'; *data++ = 'x'; *data++ = nibbleLookup[((unsigned char)s[i]) >> 4]; *data++ = nibbleLookup[((unsigned char)s[i]) & 0x0f]; break; } } *data = 0; result.resize(data - start); // Protect trailing space if (result.endsWith(' ') && type != GroupString) { result.replace(result.length() - 1, 1, "\\s"); } - result.squeeze(); return result; } char KConfigIniBackend::charFromHex(const char *str, const QFile &file, int line) { unsigned char ret = 0; for (int i = 0; i < 2; i++) { ret <<= 4; quint8 c = quint8(str[i]); if (c >= '0' && c <= '9') { ret |= c - '0'; } else if (c >= 'a' && c <= 'f') { ret |= c - 'a' + 0x0a; } else if (c >= 'A' && c <= 'F') { ret |= c - 'A' + 0x0a; } else { QByteArray e(str, 2); e.prepend("\\x"); qWarning() << warningProlog(file, line) << "Invalid hex character " << c << " in \\x-type escape sequence \"" << e.constData() << "\"."; return 'x'; } } return char(ret); } void KConfigIniBackend::printableToString(BufferFragment *aString, const QFile &file, int line) { if (aString->isEmpty() || aString->indexOf('\\') == -1) { return; } aString->trim(); int l = aString->length(); char *r = aString->data(); char *str = r; for (int i = 0; i < l; i++, r++) { if (str[i] != '\\') { *r = str[i]; } else { // Probable escape sequence i++; if (i >= l) { // Line ends after backslash - stop. *r = '\\'; break; } switch (str[i]) { case 's': *r = ' '; break; case 't': *r = '\t'; break; case 'n': *r = '\n'; break; case 'r': *r = '\r'; break; case '\\': *r = '\\'; break; case ';': // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string *r = '\\'; r++; *r = ';'; break; case 'x': if (i + 2 < l) { *r = charFromHex(str + i + 1, file, line); i += 2; } else { *r = 'x'; i = l - 1; } break; default: *r = '\\'; qWarning() << warningProlog(file, line) << QStringLiteral("Invalid escape sequence \"\\%1\".").arg(str[i]); } } } aString->truncate(r - aString->constData()); }