2 * @file Common/inifile.cpp
3 * @brief Internal implementation of the class for reading and writing parameter values in INI files.
6 * Copyright (c) 2011 Enar Vaikene
8 * This file is part of the eVaf C++ cross-platform application development framework.
10 * This file can be used under the terms of the GNU General Public License
11 * version 3.0 as published by the Free Software Foundation and appearing in
12 * the file LICENSE included in the packaging of this file. Please review the
13 * the following information to ensure the GNU General Public License version
14 * 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
16 * Alternatively, this file may be used in accordance with the Commercial License
17 * Agreement provided with the Software.
21 #include "inifile_p.h"
28 // End of line characters for Linux and Windows
35 using namespace eVaf::Common
;
37 //-------------------------------------------------------------------
39 IniFile::IniFile(QString
const & fileName
, QIODevice::OpenMode mode
)
40 : d(new Internal::IniFileImpl(fileName
, mode
))
49 bool IniFile::isValid() const
54 QString
IniFile::errorString() const
56 return d
->errorString();
59 QVariant
IniFile::getValue(QByteArray
const & paramName
, QVariant
const & defaultValue
)
61 return d
->getValue(paramName
, defaultValue
);
64 bool IniFile::setValue(QByteArray
const & paramName
, QVariant
const & value
)
66 return d
->setValue(paramName
, value
);
70 //-------------------------------------------------------------------
72 using namespace eVaf::Common::Internal
;
74 IniFileImpl::IniFileImpl(QString
const & fileName
, QIODevice::OpenMode mode
)
79 // Verify that we can open the file in the specified mode
82 mErrorString
= f
.errorString();
83 EVAF_ERROR("Failed to open INI file %s : %s", qPrintable(mFileName
), qPrintable(mErrorString
));
86 // Get and store the last modified time
88 mLastModified
= fi
.lastModified();
94 IniFileImpl::~IniFileImpl()
99 void IniFileImpl::updateCache(quint64 pos
, qint64 diff
)
101 // Walk through all the sections in the cache
102 QHash
<QByteArray
, QExplicitlySharedDataPointer
<IniFileSection
> >::const_iterator it
;
103 for (it
= mCache
.constBegin(); it
!= mCache
.constEnd(); ++it
) {
104 QExplicitlySharedDataPointer
<IniFileSection
> sectionObject
= *it
;
106 // Update the section object if it comes after the current file offset
107 if (sectionObject
->filePos
> pos
)
108 sectionObject
->filePos
+= diff
;
110 // Update individual values in the section that come after the current file offset
111 QHash
<QByteArray
, QExplicitlySharedDataPointer
<IniFileValue
> >::const_iterator it1
;
112 for (it1
= sectionObject
->values
.constBegin(); it1
!= sectionObject
->values
.constEnd(); ++it1
) {
113 QExplicitlySharedDataPointer
<IniFileValue
> valueObject
= *it1
;
114 if (valueObject
->filePos
> pos
)
115 valueObject
->filePos
+= diff
;
120 QExplicitlySharedDataPointer
<IniFileSection
> IniFileImpl::getSection(QFile
& file
, QByteArray
const & sectionName
)
122 // Check for external modifications
124 if (fi
.lastModified() != mLastModified
) {
125 // The INI file was modified externally and our internal cache is probably invalid
127 mLastModified
= fi
.lastModified();
130 // Look for the section in the cache first
131 QHash
<QByteArray
, QExplicitlySharedDataPointer
<IniFileSection
> >::const_iterator it
= mCache
.constFind(sectionName
.toLower());
132 if (it
!= mCache
.constEnd()) {
133 // Found in the cache
135 file
.seek((*it
)->filePos
);
139 // Read the INI file and look for the section
140 while (mValid
&& !file
.atEnd()) {
141 QByteArray line
= file
.readLine().trimmed();
143 // Ignore the line if it is empty, a comment or not a section name
144 if (line
.isEmpty() || line
.startsWith(';') || line
.startsWith('#') || !line
.startsWith('['))
147 // Position of the closing ']'
148 int idx
= line
.indexOf(']');
152 // Is this the section that we are looking for?
153 if (qstricmp(line
.mid(1, idx
- 1).constData(), sectionName
.constData()) == 0) {
154 // Create the section object and add to the cache
155 QExplicitlySharedDataPointer
<IniFileSection
> sectionObject(new IniFileSection(file
.pos()));
156 sectionObject
->name
= sectionName
.toLower();
157 mCache
.insert(sectionName
.toLower(), sectionObject
);
159 // Returns the section object
160 return sectionObject
;
165 // No such section found in the INI file -- return an invalid object; the file is already positioned at the end
166 return QExplicitlySharedDataPointer
<IniFileSection
>();
169 QExplicitlySharedDataPointer
<IniFileValue
> IniFileImpl::getParameter(QFile
& file
, IniFileSection
& section
, QByteArray
const & paramName
)
171 // Look for the parameter in the cache first
172 QHash
<QByteArray
, QExplicitlySharedDataPointer
<IniFileValue
> >::const_iterator it
= section
.values
.constFind(paramName
.toLower());
173 if (it
!= section
.values
.constEnd()) {
174 // Found it in the cache
176 file
.seek((*it
)->filePos
);
180 // Read the INI file and look for the parameter name
181 while (mValid
&& !file
.atEnd()) {
183 // Current file position
184 quint64 currentPos
= file
.pos();
186 QByteArray line
= file
.readLine().trimmed();
188 // Ignore the line if it is empty or a comment
189 if (line
.isEmpty() || line
.startsWith(';') || line
.startsWith('#'))
192 // Returns an invalid object if we reach the beginning of another section
193 if (line
.startsWith('[')) {
194 // Rewind to the stored position
195 file
.seek(currentPos
);
197 return QExplicitlySharedDataPointer
<IniFileValue
>();
200 // Locate '=' in the line and get the name/value pair
201 int idx
= line
.indexOf('=');
205 QByteArray name
= line
.mid(0, idx
).trimmed().toLower();
206 QByteArray value
= line
.mid(idx
+ 1).trimmed();
208 // Check for the 'windows:' or 'linux:' prefix in the parameter name
209 bool thisOsOnly
= false;
210 #if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
211 if (name
.startsWith("windows:"))
213 if (name
.startsWith("linux:")) {
219 if (name
.startsWith("linux:"))
221 if (name
.startsWith("windows:")) {
227 // If the parameter value is not in the cache, add it to the cache
228 QExplicitlySharedDataPointer
<IniFileValue
> valueObject
;
229 QHash
<QByteArray
, QExplicitlySharedDataPointer
<IniFileValue
> >::const_iterator it
= section
.values
.constFind(name
);
230 if (it
== section
.values
.constEnd()) {
231 valueObject
= new IniFileValue(currentPos
);
232 valueObject
->name
= name
;
233 valueObject
->paramValue
= value
;
234 valueObject
->thisOsOnly
= thisOsOnly
;
235 section
.values
.insert(name
, valueObject
);
238 // 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
240 valueObject
->name
= name
;
241 valueObject
->paramValue
= value
;
242 valueObject
->thisOsOnly
= thisOsOnly
;
245 // Is this the parameter vwe are looking for?
246 if (qstricmp(name
.constData(), paramName
.constData()) == 0) {
247 // Rewind to the beginning of the line
248 file
.seek(currentPos
);
250 // Return the value object
255 // 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
256 return QExplicitlySharedDataPointer
<IniFileValue
>();
259 QVariant
IniFileImpl::getValue(QByteArray
const & paramName
, QVariant
const & defaultValue
)
261 // Locate the '/' character that separates section names from key names
262 int idx
= paramName
.lastIndexOf('/');
266 // Separate section and key names
267 QByteArray section
= paramName
.left(idx
);
268 QByteArray key
= paramName
.mid(idx
+ 1);
272 if (!f
.open(mMode
)) {
273 mErrorString
= f
.errorString();
275 EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName
), qPrintable(mErrorString
));
279 // Locate the section
280 QExplicitlySharedDataPointer
<IniFileSection
> sectionObject
= getSection(f
, section
);
284 // Locate the parameter
285 QExplicitlySharedDataPointer
<IniFileValue
> valueObject
= getParameter(f
, *sectionObject
, key
);
292 // Return the cached value if it is already set and the type is the same than the default value type
293 if (valueObject
->value
.isValid() && (!defaultValue
.isValid() || valueObject
->value
.type() == defaultValue
.type()))
294 return valueObject
->value
;
296 // Convert to the proper type
297 if (defaultValue
.type() == QVariant::ByteArray
|| defaultValue
.type() == QVariant::String
) {
298 // Remove single and double quotes
299 QByteArray v
= valueObject
->paramValue
;
300 if (v
.startsWith('\"')) {
302 if (v
.endsWith('\"'))
303 v
.remove(v
.size() - 1, 1);
305 else if (v
.startsWith('\'')) {
307 if (v
.endsWith('\''))
308 v
.remove(v
.size() - 1, 1);
311 // Convert from the escaped character array
312 if (defaultValue
.type() == QVariant::String
)
313 valueObject
->value
= QVariant(strFromEscapedCharArray(v
));
315 valueObject
->value
= QVariant(binFromEscapedCharArray(v
));
318 valueObject
->value
= toVariant(valueObject
->paramValue
, defaultValue
);
320 return valueObject
->value
;
323 bool IniFileImpl::setValue(QByteArray
const & paramName
, QVariant
const & value
)
325 // Locate the '/' character that separates section names from key names
326 int idx
= paramName
.lastIndexOf('/', -1);
330 // Separate section and key names
331 QByteArray section
= paramName
.left(idx
).toLower();
332 QByteArray key
= paramName
.mid(idx
+ 1).toLower();
334 // Format the value depending on the type
335 QByteArray valueString
;
336 switch (value
.type()) {
338 valueString
= QByteArray("0x").append(QByteArray::number(value
.toUInt(), 16));
341 valueString
= QByteArray::number(value
.toInt());
343 case QVariant::Double
:
344 valueString
= QByteArray::number(value
.toDouble(), 'f');
347 valueString
= value
.toBool() ? "true" : "false";
349 case QVariant::Char
: {
350 QChar c
= value
.toChar();
351 printf("c.unicode() = %u\n", c
.unicode());
352 if (c
.unicode() < 32 || c
.unicode() >= 127)
353 valueString
= QByteArray("\\0x").append(QByteArray::number(c
.unicode(), 16));
355 valueString
= QByteArray(1, (char const)c
.unicode());
358 case QVariant::ByteArray
:
359 valueString
= binToEscapedCharArray(value
.toByteArray());
360 if (valueString
.startsWith(' ') || valueString
.endsWith(' ')) {
361 valueString
.insert(0, '\"');
362 valueString
.append('\"');
365 case QVariant::String
:
366 valueString
= strToEscapedCharArray(value
.toString());
367 if (valueString
.startsWith(' ') || valueString
.endsWith(' ')) {
368 valueString
.insert(0, '\"');
369 valueString
.append('\"');
373 valueString
= value
.toString().toLatin1();
378 if (!f
.open(mMode
)) {
379 mErrorString
= f
.errorString();
381 EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName
), qPrintable(mErrorString
));
385 mErrorString
.clear();
387 // Get the section object
388 QExplicitlySharedDataPointer
<IniFileSection
> sectionObject
= getSection(f
, section
);
390 // If the section is not found, add a new section to the end of the INI file
391 if (!sectionObject
) {
393 // Write the new section to the INI file (the file is already positioned at the end)
394 if (f
.write(QString("[%1]" EOL
).arg(section
.constData()).toLatin1()) == -1) {
395 mErrorString
= f
.errorString();
397 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName
), qPrintable(mErrorString
));
401 // Current file position
402 quint64 currentPos
= f
.pos();
404 // Add the new section to the internal cache
405 sectionObject
= new IniFileSection(currentPos
);
406 mCache
.insert(section
.toLower(), sectionObject
);
408 // Write the parameter value to the INI file
409 if (f
.write(QString("%1 = %2" EOL
).arg(key
.constData()).arg(valueString
.constData()).toLatin1()) == -1) {
410 mErrorString
= f
.errorString();
412 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName
), qPrintable(mErrorString
));
416 // Add the parameter value to the internal cache
417 QExplicitlySharedDataPointer
<IniFileValue
> valueObject(new IniFileValue(currentPos
));
418 valueObject
->name
= key
;
419 valueObject
->paramValue
= valueString
;
420 valueObject
->value
= value
;
421 sectionObject
->values
.insert(key
, valueObject
);
424 // If the section is found, use the existing section object from the cache
427 quint64 oldPos
= f
.pos();
428 QString prefix
; // Platform-specific prefix
430 // Locate the parameter value
431 QExplicitlySharedDataPointer
<IniFileValue
> valueObject
= getParameter(f
, *sectionObject
, key
);
433 // The file is now positioned either at the beginning of the parameter line or at the end of the section/file
434 currentPos
= f
.pos();
437 // Parameter was found; skip the line with the current parameter value
441 // Format the prefix string if necessary
442 if (valueObject
->thisOsOnly
) {
453 // Parameter was not found; create a new value object
454 valueObject
= new IniFileValue(currentPos
);
455 valueObject
->name
= key
;
458 // Store everything from the current position till the end of file in a temporary buffer
460 if (!tmp
.open(QBuffer::ReadWrite
)) {
461 mErrorString
= tmp
.errorString();
463 EVAF_ERROR("Failed to open the temporary buffer : %s", qPrintable(mErrorString
));
466 if (tmp
.write(f
.readAll()) == -1) {
467 mErrorString
= tmp
.errorString();
469 EVAF_ERROR("Failed to write to the temporary buffer : %s", qPrintable(mErrorString
));
473 // Rewind to the original position and write the new parameter value
475 if (f
.write(QString("%1%2 = %3" EOL
).arg(prefix
).arg(key
.constData()).arg(valueString
.constData()).toLatin1()) == -1) {
476 mErrorString
= f
.errorString();
478 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName
), qPrintable(mErrorString
));
482 // How much were sections and parameters shifted due to the new parameter value?
483 qint64 diff
= qint64(f
.pos() - oldPos
);
485 // Write everything back from the temporary buffer
487 if (f
.write(tmp
.readAll()) == -1) {
488 mErrorString
= f
.errorString();
490 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName
), qPrintable(mErrorString
));
495 // If the original file was larger than the new file, then truncate it
496 if (f
.size() > f
.pos()) {
497 if (!f
.resize(f
.pos())) {
498 mErrorString
= f
.errorString();
500 EVAF_ERROR("Failed to resize the INI file %s : %s", qPrintable(mFileName
), qPrintable(mErrorString
));
505 // If the shift was not zero, update the cache
507 updateCache(currentPos
, diff
);
509 // Update the parameter value in the internal cache
510 valueObject
->paramValue
= valueString
;
511 valueObject
->value
= value
;
517 // Update the time when the INI file was last modified
518 QFileInfo
fi(mFileName
);
519 mLastModified
= fi
.lastModified();