diff options
Diffstat (limited to 'src/core/kconfigini.cpp')
-rw-r--r-- | src/core/kconfigini.cpp | 770 |
1 files changed, 770 insertions, 0 deletions
diff --git a/src/core/kconfigini.cpp b/src/core/kconfigini.cpp new file mode 100644 index 00000000..f44b2c39 --- /dev/null +++ b/src/core/kconfigini.cpp @@ -0,0 +1,770 @@ +/* + This file is part of the KDE libraries + Copyright (c) 2006, 2007 Thomas Braxton <kde.braxton@gmail.com> + Copyright (c) 1999 Preston Brown <pbrown@kde.org> + 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 <qsavefile.h> +#include <qlockfile.h> +#include <qdatetime.h> +#include <qdir.h> +#include <qfile.h> +#include <qfileinfo.h> +#include <qdebug.h> +#include <qplatformdefs.h> + +#ifndef Q_OS_WIN +#include <unistd.h> // getuid, close +#endif +#include <sys/types.h> // uid_t +#include <fcntl.h> // open + +KCONFIGCORE_EXPORT bool kde_kiosk_exception = false; // flag to disable kiosk restrictions + +QString KConfigIniBackend::warningProlog(const QFile &file, int line) +{ + return QString::fromLatin1("KConfigIni: In file %2, line %1: ") + .arg(line).arg(file.fileName()); +} + +KConfigIniBackend::KConfigIniBackend() + : KConfigBackend(), lockFile(NULL) +{ +} + +KConfigIniBackend::~KConfigIniBackend() +{ +} + +KConfigBackend::ParseInfo + KConfigIniBackend::parseConfig(const QByteArray& currentLocale, 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& currentLocale, KEntryMap& entryMap, + ParseOptions options, bool merging) +{ + if (filePath().isEmpty() || !QFile::exists(filePath())) + return ParseOk; + + bool bDefault = options&ParseDefaults; + bool allowExecutableValues = options&ParseExpansions; + + QByteArray currentGroup("<default>"); + + QFile file(filePath()); + if (!file.open(QIODevice::ReadOnly|QIODevice::Text)) + return ParseOpenError; + + QList<QByteArray> 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; + + 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, 0, 0, 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=0; + 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) { + // 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; + 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, line.toByteArray(), entryOptions); + } else { + entryMap.setEntry(currentGroup, aKey.toByteArray(), line.toByteArray(), 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 != "<default>") == 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& currentEntry = *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")); + } + + // This is a workaround for current QLockFilePrivate::tryLock_sys + // which might crash calling qAppName() if sync() is called after + // the QCoreApplication instance is gone. It might be the case with + // KSharedConfig instances cleanup. + if (!lockFile->tryLock(lockFile->staleLockTime())) { + lockFile->removeStaleLockFile(); + lockFile->lock(); + } + return lockFile->isLocked(); +} + +void KConfigIniBackend::unlock() +{ + lockFile->unlock(); + delete lockFile; + lockFile = NULL; +} + +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<ab> escape sequences + register 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<nn>-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 '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) + << QString::fromLatin1("Invalid escape sequence \"\\%1\".").arg(str[i]); + } + } + } + aString->truncate(r - aString->constData()); +} |