--- /dev/null
+/**
+ * @file Common/inifile.cpp
+ * @brief Internal implementation of the class for reading and writing parameter values in INI files.
+ * @author Enar Vaikene
+ *
+ * Copyright (c) 2011 Enar Vaikene
+ *
+ * This file is part of the eVaf C++ cross-platform application development framework.
+ *
+ * This file can be used under the terms of the GNU General Public License
+ * version 3.0 as published by the Free Software Foundation and appearing in
+ * the file LICENSE included in the packaging of this file. Please review the
+ * the following information to ensure the GNU General Public License version
+ * 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
+ *
+ * Alternatively, this file may be used in accordance with the Commercial License
+ * Agreement provided with the Software.
+ */
+
+#include "inifile.h"
+#include "inifile_p.h"
+#include "util.h"
+#include "ilogger.h"
+
+#include <QtCore>
+
+
+// End of line characters for Linux and Windows
+#ifdef Q_OS_WIN32
+# define EOL "\r\n"
+#else
+# define EOL "\n"
+#endif
+
+using namespace eVaf::Common;
+
+//-------------------------------------------------------------------
+
+IniFile::IniFile(QString const & fileName, QIODevice::OpenMode mode)
+{
+ d = new Internal::IniFileImpl(fileName, mode);
+}
+
+IniFile::~IniFile()
+{
+ delete d;
+}
+
+bool IniFile::isValid() const
+{
+ return d->isValid();
+}
+
+QString IniFile::errorString() const
+{
+ return d->errorString();
+}
+
+QVariant IniFile::getValue(QString const & paramName, QVariant const & defaultValue)
+{
+ return d->getValue(paramName, defaultValue);
+}
+
+bool IniFile::setValue(QString const & paramName, QVariant const & value)
+{
+ return d->setValue(paramName, value);
+}
+
+
+//-------------------------------------------------------------------
+
+using namespace eVaf::Common::Internal;
+
+IniFileImpl::IniFileImpl(QString const & fileName, QIODevice::OpenMode mode)
+ : mValid(false)
+ , mFileName(fileName)
+ , mMode(mode)
+{
+ // Verify that we can open the file in the specified mode
+ QFile f(mFileName);
+ if (!f.open(mMode)) {
+ mErrorString = f.errorString();
+ EVAF_ERROR("Failed to open INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
+ }
+ else {
+ // Get and store the last modified time
+ QFileInfo fi(f);
+ mLastModified = fi.lastModified();
+ mValid = true;
+ f.close();
+ }
+}
+
+IniFileImpl::~IniFileImpl()
+{
+ mCache.clear();
+}
+
+void IniFileImpl::updateCache(quint64 pos, qint64 diff)
+{
+ // Walk through all the sections in the cache
+ QHash<QString, QExplicitlySharedDataPointer<IniFileSection> >::const_iterator it;
+ for (it = mCache.constBegin(); it != mCache.constEnd(); ++it) {
+ QExplicitlySharedDataPointer<IniFileSection> sectionObject = *it;
+
+ // Update the section object if it comes after the current file offset
+ if (sectionObject->filePos > pos)
+ sectionObject->filePos += diff;
+
+ // Update individual values in the section that come after the current file offset
+ QHash<QString, QExplicitlySharedDataPointer<IniFileValue> >::const_iterator it1;
+ for (it1 = sectionObject->values.constBegin(); it1 != sectionObject->values.constEnd(); ++it1) {
+ QExplicitlySharedDataPointer<IniFileValue> valueObject = *it1;
+ if (valueObject->filePos > pos)
+ valueObject->filePos += diff;
+ }
+ }
+}
+
+QExplicitlySharedDataPointer<IniFileSection> IniFileImpl::getSection(QFile & file, QString const & sectionName)
+{
+ // Check for external modifications
+ QFileInfo fi(file);
+ if (fi.lastModified() != mLastModified) {
+ // The INI file was modified externally and our internal cache is probably invalid
+ mCache.clear();
+ mLastModified = fi.lastModified();
+ }
+
+ // Look for the section in the cache first
+ QHash<QString, QExplicitlySharedDataPointer<IniFileSection> >::const_iterator it = mCache.constFind(sectionName.toLower());
+ if (it != mCache.constEnd()) {
+ // Found in the cache
+ if (mValid)
+ file.seek((*it)->filePos);
+ return *it;
+ }
+
+ // Read the INI file and look for the section
+ while (mValid && !file.atEnd()) {
+ QString line = file.readLine().trimmed();
+
+ // Ignore the line if it is empty, a comment or not a section name
+ if (line.isEmpty() || line.startsWith(';') || line.startsWith('#') || !line.startsWith('['))
+ continue;
+
+ // Position of the closing ']'
+ int idx = line.indexOf(']');
+ if (idx == -1)
+ continue;
+
+ // Is this the section that we are looking for?
+ if (line.mid(1, idx - 1).compare(sectionName, Qt::CaseInsensitive) == 0) {
+ // Create the section object and add to the cache
+ QExplicitlySharedDataPointer<IniFileSection> sectionObject(new IniFileSection(file.pos()));
+ sectionObject->name = sectionName.toLower();
+ mCache.insert(sectionName.toLower(), sectionObject);
+
+ // Returns the section object
+ return sectionObject;
+ }
+
+ }
+
+ // No such section found in the INI file -- return an invalid object; the file is already positioned at the end
+ return QExplicitlySharedDataPointer<IniFileSection>();
+}
+
+QExplicitlySharedDataPointer<IniFileValue> IniFileImpl::getParameter(QFile & file, IniFileSection & section, QString const & paramName)
+{
+ // Look for the parameter in the cache first
+ QHash<QString, QExplicitlySharedDataPointer<IniFileValue> >::const_iterator it = section.values.constFind(paramName.toLower());
+ if (it != section.values.constEnd()) {
+ // Found it in the cache
+ if (mValid)
+ file.seek((*it)->filePos);
+ return *it;
+ }
+
+ // Read the INI file and look for the parameter name
+ while (mValid && !file.atEnd()) {
+
+ // Current file position
+ quint64 currentPos = file.pos();
+
+ QString line = file.readLine().trimmed();
+
+ // Ignore the line if it is empty or a comment
+ if (line.isEmpty() || line.startsWith(';') || line.startsWith('#'))
+ continue;
+
+ // Returns an invalid object if we reach the beginning of another section
+ if (line.startsWith('[')) {
+ // Rewind to the stored position
+ file.seek(currentPos);
+
+ return QExplicitlySharedDataPointer<IniFileValue>();
+ }
+
+ // Locate '=' in the line and get the name/value pair
+ int idx = line.indexOf('=');
+ if (idx == -1)
+ continue;
+
+ QString name = line.mid(0, idx).trimmed().toLower();
+ QString value = line.mid(idx + 1).trimmed();
+
+ // Check for the 'windows:' or 'linux:' prefix in the parameter name
+ bool thisOsOnly = false;
+#ifdef Q_OS_LINUX
+ if (name.startsWith("windows:"))
+ continue;
+ if (name.startsWith("linux:")) {
+ name.remove(0, 6);
+ thisOsOnly = true;
+ }
+#endif
+#ifdef Q_OS_WIN32
+ if (name.startsWith("linux:"))
+ continue;
+ if (name.startsWith("windows:")) {
+ name.remove(0, 8);
+ thisOsOnly = true;
+ }
+#endif
+
+ // If the parameter value is not in the cache, add it to the cache
+ QExplicitlySharedDataPointer<IniFileValue> valueObject;
+ QHash<QString, QExplicitlySharedDataPointer<IniFileValue> >::const_iterator it = section.values.constFind(name);
+ if (it == section.values.constEnd()) {
+ valueObject = new IniFileValue(currentPos);
+ valueObject->name = name;
+ valueObject->paramValue = value;
+ section.values.insert(name, valueObject);
+ }
+ else {
+ // We should not find it in the cache, but if we somehow managed to end up here, use and update the object from the cache
+ valueObject = *it;
+ valueObject->name = name;
+ valueObject->paramValue = value;
+ }
+
+ // Is this the parameter vwe are looking for?
+ if (name.compare(paramName, Qt::CaseInsensitive) == 0) {
+ // Rewind to the beginning of the line
+ file.seek(currentPos);
+
+ // Return the value object
+ return valueObject;
+ }
+ }
+
+ // The parameter with this name was not found in the INI file -- return an invalid object; the file is already positioned at the next section or the end of the file
+ return QExplicitlySharedDataPointer<IniFileValue>();
+}
+
+QVariant IniFileImpl::getValue(QString const & paramName, QVariant const & defaultValue)
+{
+ // Locate the '/' character that separates section names from key names
+ int idx = paramName.indexOf('/');
+ if (idx < 0)
+ return defaultValue;
+
+ // Separate section and key names
+ QString section = paramName.left(idx);
+ QString key = paramName.mid(idx + 1);
+
+ // Open the file
+ QFile f(mFileName);
+ if (!f.open(mMode)) {
+ mErrorString = f.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
+ return defaultValue;
+ }
+
+ // Locate the section
+ QExplicitlySharedDataPointer<IniFileSection> sectionObject = getSection(f, section);
+ if (!sectionObject)
+ return defaultValue;
+
+ // Locate the parameter
+ QExplicitlySharedDataPointer<IniFileValue> valueObject = getParameter(f, *sectionObject, key);
+ if (!valueObject)
+ return defaultValue;
+
+ if (f.isOpen())
+ f.close();
+
+ return toVariant(valueObject->paramValue, defaultValue);
+}
+
+bool IniFileImpl::setValue(QString const & paramName, QVariant const & value)
+{
+ // Locate the '/' character that separates section names from key names
+ int idx = paramName.indexOf('/');
+ if (idx < 0)
+ return false;
+
+ // Separate section and key names
+ QString section = paramName.left(idx).toLower();
+ QString key = paramName.mid(idx + 1).toLower();
+
+ // Format the value depending on the type
+ QString valueString;
+ switch (value.type()) {
+ case QVariant::UInt:
+ valueString = "0x" + QString::number(value.toUInt(), 16);
+ break;
+ case QVariant::Int:
+ valueString = QString::number(value.toInt());
+ break;
+ case QVariant::Double:
+ valueString = QString::number(value.toDouble(), 'f');
+ break;
+ case QVariant::Bool:
+ valueString = value.toBool() ? "true" : "false";
+ break;
+ case QVariant::Char:
+ valueString = value.toChar();
+ break;
+ default:
+ valueString = value.toString();
+ }
+
+ // Open the file
+ QFile f(mFileName);
+ if (!f.open(mMode)) {
+ mErrorString = f.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
+ return false;
+ }
+ mValid = true;
+ mErrorString.clear();
+
+ // Get the section object
+ QExplicitlySharedDataPointer<IniFileSection> sectionObject = getSection(f, section);
+
+ // If the section is not found, add a new section to the end of the INI file
+ if (!sectionObject) {
+
+ // Write the new section to the INI file (the file is already positioned at the end)
+ if (f.write(QString("[%1]" EOL).arg(section).toLocal8Bit()) == -1) {
+ mErrorString = f.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
+ return false;
+ }
+
+ // Current file position
+ quint64 currentPos = f.pos();
+
+ // Add the new section to the internal cache
+ sectionObject = new IniFileSection(currentPos);
+ mCache.insert(section.toLower(), sectionObject);
+
+ // Write the parameter value to the INI file
+ if (f.write(QString("%1 = %2" EOL).arg(key).arg(valueString).toLocal8Bit()) == -1) {
+ mErrorString = f.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
+ return false;
+ }
+
+ // Add the parameter value to the internal cache
+ QExplicitlySharedDataPointer<IniFileValue> valueObject(new IniFileValue(currentPos));
+ valueObject->name = key;
+ valueObject->paramValue = valueString;
+ sectionObject->values.insert(key, valueObject);
+ }
+
+ // If the section is found, use the existing section object from the cache
+ else {
+ quint64 currentPos;
+ quint64 oldPos = f.pos();
+ QString prefix; // Platform-specific prefix
+
+ // Locate the parameter value
+ QExplicitlySharedDataPointer<IniFileValue> valueObject = getParameter(f, *sectionObject, key);
+
+ // The file is now positioned either at the beginning of the parameter line or at the end of the section/file
+ currentPos = f.pos();
+
+ if (valueObject) {
+ // Parameter was found; skip the line with the current parameter value
+ f.readLine();
+ oldPos = f.pos();
+
+ // Format the prefix string if necessary
+ if (valueObject->thisOsOnly) {
+#ifdef Q_OS_LINUX
+ prefix = "linux:";
+#endif
+#if Q_OS_WIN32
+ prefix = "windows:";
+#endif
+ }
+ }
+
+ else {
+ // Parameter was not found; create a new value object
+ valueObject = new IniFileValue(currentPos);
+ valueObject->name = key;
+ }
+
+ // Store everything from the current position till the end of file in a temporary buffer
+ QBuffer tmp;
+ if (!tmp.open(QBuffer::ReadWrite)) {
+ mErrorString = tmp.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to open the temporary buffer : %s", qPrintable(mErrorString));
+ return false;
+ }
+ if (tmp.write(f.readAll()) == -1) {
+ mErrorString = tmp.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to write to the temporary buffer : %s", qPrintable(mErrorString));
+ return false;
+ }
+
+ // Rewind to the original position and write the new parameter value
+ f.seek(currentPos);
+ if (f.write(QString("%1%2 = %3" EOL).arg(prefix).arg(key).arg(valueString).toLocal8Bit()) == -1) {
+ mErrorString = f.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
+ return false;
+ }
+
+ // How much were sections and parameters shifted due to the new parameter value?
+ qint64 diff = qint64(f.pos() - oldPos);
+
+ // Write everything back from the temporary buffer
+ tmp.seek(0);
+ if (f.write(tmp.readAll()) == -1) {
+ mErrorString = f.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
+ return false;
+ }
+ tmp.close();
+
+ // If the original file was larger than the new file, then truncate it
+ if (f.size() > f.pos()) {
+ if (!f.resize(f.pos())) {
+ mErrorString = f.errorString();
+ mValid = false;
+ EVAF_ERROR("Failed to resize the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
+ return false;
+ }
+ }
+
+ // If the shift was not zero, update the cache
+ if (diff)
+ updateCache(currentPos, diff);
+
+ }
+
+ f.close();
+
+ // Update the time when the INI file was last modified
+ QFileInfo fi(mFileName);
+ mLastModified = fi.lastModified();
+
+ return true;
+}
--- /dev/null
+/**
+ * @file Common/inifile_p.h
+ * @brief Internal implementation of the class for reading and writing parameter values in INI files.
+ * @author Enar Vaikene
+ *
+ * Copyright (c) 2011 Enar Vaikene
+ *
+ * This file is part of the eVaf C++ cross-platform application development framework.
+ *
+ * This file can be used under the terms of the GNU General Public License
+ * version 3.0 as published by the Free Software Foundation and appearing in
+ * the file LICENSE included in the packaging of this file. Please review the
+ * the following information to ensure the GNU General Public License version
+ * 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
+ *
+ * Alternatively, this file may be used in accordance with the Commercial License
+ * Agreement provided with the Software.
+ */
+
+#ifndef __COMMON_INIFILE_P_H
+#define __COMMON_INIFILE_P_H
+
+#include <QSharedData>
+#include <QExplicitlySharedDataPointer>
+#include <QHash>
+#include <QString>
+#include <QVariant>
+#include <QDateTime>
+#include <QFile>
+
+
+namespace eVaf {
+namespace Common {
+namespace Internal {
+
+/**
+ * INI file value in the internal cache
+ */
+class IniFileValue : public QSharedData
+{
+public:
+
+ IniFileValue(quint64 pos)
+ : QSharedData()
+ , filePos(pos)
+ , thisOsOnly(false)
+ {}
+
+ /**
+ * File position
+ *
+ * Offset of the parameter in the INI file. By seeking the file to this offset value,
+ * the next character read or written will be the beginning of the key name.
+ */
+ quint64 filePos;
+
+ /**
+ * Key name of the parameter
+ */
+ QString name;
+
+ /**
+ * Value from the INI file
+ */
+ QString paramValue;
+
+ /**
+ * Flag indicating that this value is valid on this OS only
+ */
+ bool thisOsOnly;
+};
+
+/**
+ * INI file section in the internal cache
+ */
+class IniFileSection : public QSharedData
+{
+public:
+
+ IniFileSection(quint64 pos)
+ : QSharedData()
+ , filePos(pos)
+ {}
+
+ /**
+ * File position
+ *
+ * Offset of the section in the INI file. By seeking the file to this offset value,
+ * the next character read or written will be the first character of the section.
+ */
+ quint64 filePos;
+
+ /**
+ * Name of the section
+ */
+ QString name;
+
+ /**
+ * List of all the known parameter values in this section
+ *
+ * The key to the hash table is the name of the key for the parameters value.
+ */
+ QHash<QString, QExplicitlySharedDataPointer<IniFileValue> > values;
+
+};
+
+/**
+ * Internal implementation of the IniFile class.
+ */
+class IniFileImpl
+{
+public:
+
+ IniFileImpl(QString const & fileName, QIODevice::OpenMode mode);
+
+ ~IniFileImpl();
+
+ QVariant getValue(QString const & paramName, QVariant const & defaultValue);
+
+ bool setValue(QString const & paramName, QVariant const & value);
+
+ inline bool isValid() const { return mValid; }
+
+ inline QString const & fileName() const { return mFileName; }
+
+ inline QString const & errorString() const { return mErrorString; }
+
+
+private: // Members
+
+ /// Flag indicating that the INI file object is valid and can be used to access the INI file
+ bool mValid;
+
+ /// Name of the INI file
+ QString mFileName;
+
+ /// INI file opening mode
+ QIODevice::OpenMode mMode;
+
+ /// Last human-readable error message if an operation with the INI file failed
+ QString mErrorString;
+
+ /**
+ * Internal cache of sections and parameter values.
+ *
+ * The key to the hash table is the name of the section.
+ */
+ QHash<QString, QExplicitlySharedDataPointer<IniFileSection> > mCache;
+
+ /// When was the INI file modified.
+ QDateTime mLastModified;
+
+
+private: /// Methods
+
+ /**
+ * Updates items in the internal cache after changes to the INI file
+ * @param pos File offset from where the cache should be updated
+ * @param diff Difference between old and new file offsets
+ *
+ * When a parameter value is modified or a new value inserted, the length of the INI file changes.
+ * Sections and parameters that come after the modified parameter value are shifted and their file
+ * positions changed. This method updates items in the internal cache after changes to the INI file.
+ */
+ void updateCache(quint64 pos, qint64 diff);
+
+ /**
+ * Looks for a section in the INI file
+ * @param file The file object
+ * @param sectionName Name of the section
+ * @return The section object or an invalid object if not found
+ *
+ * This method reads the INI file and looks for the section with the given name. If found, returns the section
+ * object and seeks the file to the first character after the section name.
+ *
+ * If the section is not found, returns an invalid section object and seeks the file to the end of the file.
+ *
+ * The file object is expected to be opened if the mValid flag is true. If the mValid flag is false, looks
+ * only in the cache.
+ */
+ QExplicitlySharedDataPointer<IniFileSection> getSection(QFile & file, QString const & sectionName);
+
+ /**
+ * Looks for a parameter in the INI file
+ * @param file The file object
+ * @param section The section object
+ * @param paramName Name of the parameter
+ * @return The value object or an invalid object if not found
+ *
+ * This method reads the INI file and looks for the parameter with the given name. If found, returns the value
+ * object and seeks the file to the beginning of the line containing the parameter.
+ *
+ * If not found, returns an invalid value object and seeks the file to the end of the section.
+ *
+ * The file object is expected to be opened if the mValid flag is true. If the mValid flag is false, looks
+ * only in the cache.
+ */
+ QExplicitlySharedDataPointer<IniFileValue> getParameter(QFile & file, IniFileSection & section, QString const & paramName);
+
+};
+
+
+} // namespace eVaf::Common::Internal
+} // namespace eVaf::Common
+} // namespace eVaf
+
+#endif // inifile_p.h