/** * @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-2019 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 // 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() { d.reset(); } bool IniFile::isValid() const { return d->isValid(); } QString IniFile::errorString() const { return d->errorString(); } QVariant IniFile::getValue(QByteArray const & paramName, QVariant const & defaultValue) { return d->getValue(paramName, defaultValue); } bool IniFile::setValue(QByteArray 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(qint64 pos, qint64 diff) { // Walk through all the sections in the cache QHash >::const_iterator it; for (it = mCache.constBegin(); it != mCache.constEnd(); ++it) { QExplicitlySharedDataPointer 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 >::const_iterator it1; for (it1 = sectionObject->values.constBegin(); it1 != sectionObject->values.constEnd(); ++it1) { QExplicitlySharedDataPointer valueObject = *it1; if (valueObject->filePos > pos) valueObject->filePos += diff; } } } QExplicitlySharedDataPointer IniFileImpl::getSection(QFile & file, QByteArray 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 >::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()) { QByteArray 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 (qstricmp(line.mid(1, idx - 1).constData(), sectionName.constData()) == 0) { // Create the section object and add to the cache QExplicitlySharedDataPointer 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(); } QExplicitlySharedDataPointer IniFileImpl::getParameter(QFile & file, IniFileSection & section, QByteArray const & paramName) { // Look for the parameter in the cache first QHash >::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 qint64 currentPos = file.pos(); QByteArray 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(); } // Locate '=' in the line and get the name/value pair int idx = line.indexOf('='); if (idx == -1) continue; QByteArray name = line.mid(0, idx).trimmed().toLower(); QByteArray value = line.mid(idx + 1).trimmed(); // Check for the 'windows:' or 'linux:' prefix in the parameter name bool thisOsOnly = false; #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) 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 valueObject; QHash >::const_iterator it = section.values.constFind(name); if (it == section.values.constEnd()) { valueObject = new IniFileValue(currentPos); valueObject->name = name; valueObject->paramValue = value; valueObject->thisOsOnly = thisOsOnly; 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; valueObject->thisOsOnly = thisOsOnly; } // Is this the parameter vwe are looking for? if (qstricmp(name.constData(), paramName.constData()) == 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(); } QVariant IniFileImpl::getValue(QByteArray const & paramName, QVariant const & defaultValue) { // Locate the '/' character that separates section names from key names int idx = paramName.lastIndexOf('/'); if (idx < 0) return defaultValue; // Separate section and key names QByteArray section = paramName.left(idx); QByteArray 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 sectionObject = getSection(f, section); if (!sectionObject) return defaultValue; // Locate the parameter QExplicitlySharedDataPointer valueObject = getParameter(f, *sectionObject, key); if (!valueObject) return defaultValue; if (f.isOpen()) f.close(); // Return the cached value if it is already set and the type is the same than the default value type if (valueObject->value.isValid() && (!defaultValue.isValid() || valueObject->value.type() == defaultValue.type())) return valueObject->value; // Convert to the proper type if (defaultValue.type() == QVariant::ByteArray || defaultValue.type() == QVariant::String) { // Remove single and double quotes QByteArray v = valueObject->paramValue; if (v.startsWith('\"')) { v.remove(0, 1); if (v.endsWith('\"')) v.remove(v.size() - 1, 1); } else if (v.startsWith('\'')) { v.remove(0, 1); if (v.endsWith('\'')) v.remove(v.size() - 1, 1); } // Convert from the escaped character array if (defaultValue.type() == QVariant::String) valueObject->value = QVariant(strFromEscapedCharArray(v)); else valueObject->value = QVariant(binFromEscapedCharArray(v)); } else valueObject->value = toVariant(valueObject->paramValue, defaultValue); return valueObject->value; } bool IniFileImpl::setValue(QByteArray const & paramName, QVariant const & value) { // Locate the '/' character that separates section names from key names int idx = paramName.lastIndexOf('/', -1); if (idx < 0) return false; // Separate section and key names QByteArray section = paramName.left(idx).toLower(); QByteArray key = paramName.mid(idx + 1).toLower(); // Format the value depending on the type QByteArray valueString; switch (value.type()) { case QVariant::UInt: valueString = QByteArray("0x").append(QByteArray::number(value.toUInt(), 16)); break; case QVariant::Int: valueString = QByteArray::number(value.toInt()); break; case QVariant::Double: valueString = QByteArray::number(value.toDouble(), 'f'); break; case QVariant::Bool: valueString = value.toBool() ? "true" : "false"; break; case QVariant::Char: { QChar c = value.toChar(); printf("c.unicode() = %u\n", c.unicode()); if (c.unicode() < 32 || c.unicode() >= 127) valueString = QByteArray("\\0x").append(QByteArray::number(c.unicode(), 16)); else valueString = QByteArray(1, static_cast(c.unicode())); break; } case QVariant::ByteArray: valueString = binToEscapedCharArray(value.toByteArray()); if (valueString.startsWith(' ') || valueString.endsWith(' ')) { valueString.insert(0, '\"'); valueString.append('\"'); } break; case QVariant::String: valueString = strToEscapedCharArray(value.toString()); if (valueString.startsWith(' ') || valueString.endsWith(' ')) { valueString.insert(0, '\"'); valueString.append('\"'); } break; default: valueString = value.toString().toLatin1(); } // 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 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.constData()).toLatin1()) == -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 qint64 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.constData()).arg(valueString.constData()).toLatin1()) == -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 valueObject(new IniFileValue(currentPos)); valueObject->name = key; valueObject->paramValue = valueString; valueObject->value = value; sectionObject->values.insert(key, valueObject); } // If the section is found, use the existing section object from the cache else { qint64 currentPos; qint64 oldPos = f.pos(); QString prefix; // Platform-specific prefix // Locate the parameter value QExplicitlySharedDataPointer 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 #ifdef 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.constData()).arg(valueString.constData()).toLatin1()) == -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); // Update the parameter value in the internal cache valueObject->paramValue = valueString; valueObject->value = value; } f.close(); // Update the time when the INI file was last modified QFileInfo fi(mFileName); mLastModified = fi.lastModified(); return true; }