]> vaikene.ee Git - evaf/blob - src/libs/Common/inifile.cpp
Implemented IniFile class that reads and writes parameters values in INI files.
[evaf] / src / libs / Common / inifile.cpp
1 /**
2 * @file Common/inifile.cpp
3 * @brief Internal implementation of the class for reading and writing parameter values in INI files.
4 * @author Enar Vaikene
5 *
6 * Copyright (c) 2011 Enar Vaikene
7 *
8 * This file is part of the eVaf C++ cross-platform application development framework.
9 *
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.
15 *
16 * Alternatively, this file may be used in accordance with the Commercial License
17 * Agreement provided with the Software.
18 */
19
20 #include "inifile.h"
21 #include "inifile_p.h"
22 #include "util.h"
23 #include "ilogger.h"
24
25 #include <QtCore>
26
27
28 // End of line characters for Linux and Windows
29 #ifdef Q_OS_WIN32
30 # define EOL "\r\n"
31 #else
32 # define EOL "\n"
33 #endif
34
35 using namespace eVaf::Common;
36
37 //-------------------------------------------------------------------
38
39 IniFile::IniFile(QString const & fileName, QIODevice::OpenMode mode)
40 {
41 d = new Internal::IniFileImpl(fileName, mode);
42 }
43
44 IniFile::~IniFile()
45 {
46 delete d;
47 }
48
49 bool IniFile::isValid() const
50 {
51 return d->isValid();
52 }
53
54 QString IniFile::errorString() const
55 {
56 return d->errorString();
57 }
58
59 QVariant IniFile::getValue(QString const & paramName, QVariant const & defaultValue)
60 {
61 return d->getValue(paramName, defaultValue);
62 }
63
64 bool IniFile::setValue(QString const & paramName, QVariant const & value)
65 {
66 return d->setValue(paramName, value);
67 }
68
69
70 //-------------------------------------------------------------------
71
72 using namespace eVaf::Common::Internal;
73
74 IniFileImpl::IniFileImpl(QString const & fileName, QIODevice::OpenMode mode)
75 : mValid(false)
76 , mFileName(fileName)
77 , mMode(mode)
78 {
79 // Verify that we can open the file in the specified mode
80 QFile f(mFileName);
81 if (!f.open(mMode)) {
82 mErrorString = f.errorString();
83 EVAF_ERROR("Failed to open INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
84 }
85 else {
86 // Get and store the last modified time
87 QFileInfo fi(f);
88 mLastModified = fi.lastModified();
89 mValid = true;
90 f.close();
91 }
92 }
93
94 IniFileImpl::~IniFileImpl()
95 {
96 mCache.clear();
97 }
98
99 void IniFileImpl::updateCache(quint64 pos, qint64 diff)
100 {
101 // Walk through all the sections in the cache
102 QHash<QString, QExplicitlySharedDataPointer<IniFileSection> >::const_iterator it;
103 for (it = mCache.constBegin(); it != mCache.constEnd(); ++it) {
104 QExplicitlySharedDataPointer<IniFileSection> sectionObject = *it;
105
106 // Update the section object if it comes after the current file offset
107 if (sectionObject->filePos > pos)
108 sectionObject->filePos += diff;
109
110 // Update individual values in the section that come after the current file offset
111 QHash<QString, 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;
116 }
117 }
118 }
119
120 QExplicitlySharedDataPointer<IniFileSection> IniFileImpl::getSection(QFile & file, QString const & sectionName)
121 {
122 // Check for external modifications
123 QFileInfo fi(file);
124 if (fi.lastModified() != mLastModified) {
125 // The INI file was modified externally and our internal cache is probably invalid
126 mCache.clear();
127 mLastModified = fi.lastModified();
128 }
129
130 // Look for the section in the cache first
131 QHash<QString, QExplicitlySharedDataPointer<IniFileSection> >::const_iterator it = mCache.constFind(sectionName.toLower());
132 if (it != mCache.constEnd()) {
133 // Found in the cache
134 if (mValid)
135 file.seek((*it)->filePos);
136 return *it;
137 }
138
139 // Read the INI file and look for the section
140 while (mValid && !file.atEnd()) {
141 QString line = file.readLine().trimmed();
142
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('['))
145 continue;
146
147 // Position of the closing ']'
148 int idx = line.indexOf(']');
149 if (idx == -1)
150 continue;
151
152 // Is this the section that we are looking for?
153 if (line.mid(1, idx - 1).compare(sectionName, Qt::CaseInsensitive) == 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);
158
159 // Returns the section object
160 return sectionObject;
161 }
162
163 }
164
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>();
167 }
168
169 QExplicitlySharedDataPointer<IniFileValue> IniFileImpl::getParameter(QFile & file, IniFileSection & section, QString const & paramName)
170 {
171 // Look for the parameter in the cache first
172 QHash<QString, QExplicitlySharedDataPointer<IniFileValue> >::const_iterator it = section.values.constFind(paramName.toLower());
173 if (it != section.values.constEnd()) {
174 // Found it in the cache
175 if (mValid)
176 file.seek((*it)->filePos);
177 return *it;
178 }
179
180 // Read the INI file and look for the parameter name
181 while (mValid && !file.atEnd()) {
182
183 // Current file position
184 quint64 currentPos = file.pos();
185
186 QString line = file.readLine().trimmed();
187
188 // Ignore the line if it is empty or a comment
189 if (line.isEmpty() || line.startsWith(';') || line.startsWith('#'))
190 continue;
191
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);
196
197 return QExplicitlySharedDataPointer<IniFileValue>();
198 }
199
200 // Locate '=' in the line and get the name/value pair
201 int idx = line.indexOf('=');
202 if (idx == -1)
203 continue;
204
205 QString name = line.mid(0, idx).trimmed().toLower();
206 QString value = line.mid(idx + 1).trimmed();
207
208 // Check for the 'windows:' or 'linux:' prefix in the parameter name
209 bool thisOsOnly = false;
210 #ifdef Q_OS_LINUX
211 if (name.startsWith("windows:"))
212 continue;
213 if (name.startsWith("linux:")) {
214 name.remove(0, 6);
215 thisOsOnly = true;
216 }
217 #endif
218 #ifdef Q_OS_WIN32
219 if (name.startsWith("linux:"))
220 continue;
221 if (name.startsWith("windows:")) {
222 name.remove(0, 8);
223 thisOsOnly = true;
224 }
225 #endif
226
227 // If the parameter value is not in the cache, add it to the cache
228 QExplicitlySharedDataPointer<IniFileValue> valueObject;
229 QHash<QString, 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 section.values.insert(name, valueObject);
235 }
236 else {
237 // 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
238 valueObject = *it;
239 valueObject->name = name;
240 valueObject->paramValue = value;
241 }
242
243 // Is this the parameter vwe are looking for?
244 if (name.compare(paramName, Qt::CaseInsensitive) == 0) {
245 // Rewind to the beginning of the line
246 file.seek(currentPos);
247
248 // Return the value object
249 return valueObject;
250 }
251 }
252
253 // 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
254 return QExplicitlySharedDataPointer<IniFileValue>();
255 }
256
257 QVariant IniFileImpl::getValue(QString const & paramName, QVariant const & defaultValue)
258 {
259 // Locate the '/' character that separates section names from key names
260 int idx = paramName.indexOf('/');
261 if (idx < 0)
262 return defaultValue;
263
264 // Separate section and key names
265 QString section = paramName.left(idx);
266 QString key = paramName.mid(idx + 1);
267
268 // Open the file
269 QFile f(mFileName);
270 if (!f.open(mMode)) {
271 mErrorString = f.errorString();
272 mValid = false;
273 EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
274 return defaultValue;
275 }
276
277 // Locate the section
278 QExplicitlySharedDataPointer<IniFileSection> sectionObject = getSection(f, section);
279 if (!sectionObject)
280 return defaultValue;
281
282 // Locate the parameter
283 QExplicitlySharedDataPointer<IniFileValue> valueObject = getParameter(f, *sectionObject, key);
284 if (!valueObject)
285 return defaultValue;
286
287 if (f.isOpen())
288 f.close();
289
290 return toVariant(valueObject->paramValue, defaultValue);
291 }
292
293 bool IniFileImpl::setValue(QString const & paramName, QVariant const & value)
294 {
295 // Locate the '/' character that separates section names from key names
296 int idx = paramName.indexOf('/');
297 if (idx < 0)
298 return false;
299
300 // Separate section and key names
301 QString section = paramName.left(idx).toLower();
302 QString key = paramName.mid(idx + 1).toLower();
303
304 // Format the value depending on the type
305 QString valueString;
306 switch (value.type()) {
307 case QVariant::UInt:
308 valueString = "0x" + QString::number(value.toUInt(), 16);
309 break;
310 case QVariant::Int:
311 valueString = QString::number(value.toInt());
312 break;
313 case QVariant::Double:
314 valueString = QString::number(value.toDouble(), 'f');
315 break;
316 case QVariant::Bool:
317 valueString = value.toBool() ? "true" : "false";
318 break;
319 case QVariant::Char:
320 valueString = value.toChar();
321 break;
322 default:
323 valueString = value.toString();
324 }
325
326 // Open the file
327 QFile f(mFileName);
328 if (!f.open(mMode)) {
329 mErrorString = f.errorString();
330 mValid = false;
331 EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
332 return false;
333 }
334 mValid = true;
335 mErrorString.clear();
336
337 // Get the section object
338 QExplicitlySharedDataPointer<IniFileSection> sectionObject = getSection(f, section);
339
340 // If the section is not found, add a new section to the end of the INI file
341 if (!sectionObject) {
342
343 // Write the new section to the INI file (the file is already positioned at the end)
344 if (f.write(QString("[%1]" EOL).arg(section).toLocal8Bit()) == -1) {
345 mErrorString = f.errorString();
346 mValid = false;
347 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
348 return false;
349 }
350
351 // Current file position
352 quint64 currentPos = f.pos();
353
354 // Add the new section to the internal cache
355 sectionObject = new IniFileSection(currentPos);
356 mCache.insert(section.toLower(), sectionObject);
357
358 // Write the parameter value to the INI file
359 if (f.write(QString("%1 = %2" EOL).arg(key).arg(valueString).toLocal8Bit()) == -1) {
360 mErrorString = f.errorString();
361 mValid = false;
362 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
363 return false;
364 }
365
366 // Add the parameter value to the internal cache
367 QExplicitlySharedDataPointer<IniFileValue> valueObject(new IniFileValue(currentPos));
368 valueObject->name = key;
369 valueObject->paramValue = valueString;
370 sectionObject->values.insert(key, valueObject);
371 }
372
373 // If the section is found, use the existing section object from the cache
374 else {
375 quint64 currentPos;
376 quint64 oldPos = f.pos();
377 QString prefix; // Platform-specific prefix
378
379 // Locate the parameter value
380 QExplicitlySharedDataPointer<IniFileValue> valueObject = getParameter(f, *sectionObject, key);
381
382 // The file is now positioned either at the beginning of the parameter line or at the end of the section/file
383 currentPos = f.pos();
384
385 if (valueObject) {
386 // Parameter was found; skip the line with the current parameter value
387 f.readLine();
388 oldPos = f.pos();
389
390 // Format the prefix string if necessary
391 if (valueObject->thisOsOnly) {
392 #ifdef Q_OS_LINUX
393 prefix = "linux:";
394 #endif
395 #if Q_OS_WIN32
396 prefix = "windows:";
397 #endif
398 }
399 }
400
401 else {
402 // Parameter was not found; create a new value object
403 valueObject = new IniFileValue(currentPos);
404 valueObject->name = key;
405 }
406
407 // Store everything from the current position till the end of file in a temporary buffer
408 QBuffer tmp;
409 if (!tmp.open(QBuffer::ReadWrite)) {
410 mErrorString = tmp.errorString();
411 mValid = false;
412 EVAF_ERROR("Failed to open the temporary buffer : %s", qPrintable(mErrorString));
413 return false;
414 }
415 if (tmp.write(f.readAll()) == -1) {
416 mErrorString = tmp.errorString();
417 mValid = false;
418 EVAF_ERROR("Failed to write to the temporary buffer : %s", qPrintable(mErrorString));
419 return false;
420 }
421
422 // Rewind to the original position and write the new parameter value
423 f.seek(currentPos);
424 if (f.write(QString("%1%2 = %3" EOL).arg(prefix).arg(key).arg(valueString).toLocal8Bit()) == -1) {
425 mErrorString = f.errorString();
426 mValid = false;
427 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
428 return false;
429 }
430
431 // How much were sections and parameters shifted due to the new parameter value?
432 qint64 diff = qint64(f.pos() - oldPos);
433
434 // Write everything back from the temporary buffer
435 tmp.seek(0);
436 if (f.write(tmp.readAll()) == -1) {
437 mErrorString = f.errorString();
438 mValid = false;
439 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
440 return false;
441 }
442 tmp.close();
443
444 // If the original file was larger than the new file, then truncate it
445 if (f.size() > f.pos()) {
446 if (!f.resize(f.pos())) {
447 mErrorString = f.errorString();
448 mValid = false;
449 EVAF_ERROR("Failed to resize the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
450 return false;
451 }
452 }
453
454 // If the shift was not zero, update the cache
455 if (diff)
456 updateCache(currentPos, diff);
457
458 }
459
460 f.close();
461
462 // Update the time when the INI file was last modified
463 QFileInfo fi(mFileName);
464 mLastModified = fi.lastModified();
465
466 return true;
467 }