]> vaikene.ee Git - evaf/blob - src/libs/Common/inifile.cpp
14a47ee5fda19cb3c82b2bdbab383f13cfe3d9a6
[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 valueObject->thisOsOnly = thisOsOnly;
235 section.values.insert(name, valueObject);
236 }
237 else {
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
239 valueObject = *it;
240 valueObject->name = name;
241 valueObject->paramValue = value;
242 valueObject->thisOsOnly = thisOsOnly;
243 }
244
245 // Is this the parameter vwe are looking for?
246 if (name.compare(paramName, Qt::CaseInsensitive) == 0) {
247 // Rewind to the beginning of the line
248 file.seek(currentPos);
249
250 // Return the value object
251 return valueObject;
252 }
253 }
254
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>();
257 }
258
259 QVariant IniFileImpl::getValue(QString const & paramName, QVariant const & defaultValue)
260 {
261 // Locate the '/' character that separates section names from key names
262 int idx = paramName.lastIndexOf('/');
263 if (idx < 0)
264 return defaultValue;
265
266 // Separate section and key names
267 QString section = paramName.left(idx);
268 QString key = paramName.mid(idx + 1);
269
270 // Open the file
271 QFile f(mFileName);
272 if (!f.open(mMode)) {
273 mErrorString = f.errorString();
274 mValid = false;
275 EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
276 return defaultValue;
277 }
278
279 // Locate the section
280 QExplicitlySharedDataPointer<IniFileSection> sectionObject = getSection(f, section);
281 if (!sectionObject)
282 return defaultValue;
283
284 // Locate the parameter
285 QExplicitlySharedDataPointer<IniFileValue> valueObject = getParameter(f, *sectionObject, key);
286 if (!valueObject)
287 return defaultValue;
288
289 if (f.isOpen())
290 f.close();
291
292 return toVariant(valueObject->paramValue, defaultValue);
293 }
294
295 bool IniFileImpl::setValue(QString const & paramName, QVariant const & value)
296 {
297 // Locate the '/' character that separates section names from key names
298 int idx = paramName.lastIndexOf('/', -1);
299 if (idx < 0)
300 return false;
301
302 // Separate section and key names
303 QString section = paramName.left(idx).toLower();
304 QString key = paramName.mid(idx + 1).toLower();
305
306 // Format the value depending on the type
307 QString valueString;
308 switch (value.type()) {
309 case QVariant::UInt:
310 valueString = "0x" + QString::number(value.toUInt(), 16);
311 break;
312 case QVariant::Int:
313 valueString = QString::number(value.toInt());
314 break;
315 case QVariant::Double:
316 valueString = QString::number(value.toDouble(), 'f');
317 break;
318 case QVariant::Bool:
319 valueString = value.toBool() ? "true" : "false";
320 break;
321 case QVariant::Char:
322 valueString = value.toChar();
323 break;
324 default:
325 valueString = value.toString();
326 }
327
328 // Open the file
329 QFile f(mFileName);
330 if (!f.open(mMode)) {
331 mErrorString = f.errorString();
332 mValid = false;
333 EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
334 return false;
335 }
336 mValid = true;
337 mErrorString.clear();
338
339 // Get the section object
340 QExplicitlySharedDataPointer<IniFileSection> sectionObject = getSection(f, section);
341
342 // If the section is not found, add a new section to the end of the INI file
343 if (!sectionObject) {
344
345 // Write the new section to the INI file (the file is already positioned at the end)
346 if (f.write(QString("[%1]" EOL).arg(section).toLocal8Bit()) == -1) {
347 mErrorString = f.errorString();
348 mValid = false;
349 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
350 return false;
351 }
352
353 // Current file position
354 quint64 currentPos = f.pos();
355
356 // Add the new section to the internal cache
357 sectionObject = new IniFileSection(currentPos);
358 mCache.insert(section.toLower(), sectionObject);
359
360 // Write the parameter value to the INI file
361 if (f.write(QString("%1 = %2" EOL).arg(key).arg(valueString).toLocal8Bit()) == -1) {
362 mErrorString = f.errorString();
363 mValid = false;
364 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
365 return false;
366 }
367
368 // Add the parameter value to the internal cache
369 QExplicitlySharedDataPointer<IniFileValue> valueObject(new IniFileValue(currentPos));
370 valueObject->name = key;
371 valueObject->paramValue = valueString;
372 sectionObject->values.insert(key, valueObject);
373 }
374
375 // If the section is found, use the existing section object from the cache
376 else {
377 quint64 currentPos;
378 quint64 oldPos = f.pos();
379 QString prefix; // Platform-specific prefix
380
381 // Locate the parameter value
382 QExplicitlySharedDataPointer<IniFileValue> valueObject = getParameter(f, *sectionObject, key);
383
384 // The file is now positioned either at the beginning of the parameter line or at the end of the section/file
385 currentPos = f.pos();
386
387 if (valueObject) {
388 // Parameter was found; skip the line with the current parameter value
389 f.readLine();
390 oldPos = f.pos();
391
392 // Format the prefix string if necessary
393 if (valueObject->thisOsOnly) {
394 #ifdef Q_OS_LINUX
395 prefix = "linux:";
396 #endif
397 #ifdef Q_OS_WIN32
398 prefix = "windows:";
399 #endif
400 }
401 }
402
403 else {
404 // Parameter was not found; create a new value object
405 valueObject = new IniFileValue(currentPos);
406 valueObject->name = key;
407 }
408
409 // Store everything from the current position till the end of file in a temporary buffer
410 QBuffer tmp;
411 if (!tmp.open(QBuffer::ReadWrite)) {
412 mErrorString = tmp.errorString();
413 mValid = false;
414 EVAF_ERROR("Failed to open the temporary buffer : %s", qPrintable(mErrorString));
415 return false;
416 }
417 if (tmp.write(f.readAll()) == -1) {
418 mErrorString = tmp.errorString();
419 mValid = false;
420 EVAF_ERROR("Failed to write to the temporary buffer : %s", qPrintable(mErrorString));
421 return false;
422 }
423
424 // Rewind to the original position and write the new parameter value
425 f.seek(currentPos);
426 if (f.write(QString("%1%2 = %3" EOL).arg(prefix).arg(key).arg(valueString).toLocal8Bit()) == -1) {
427 mErrorString = f.errorString();
428 mValid = false;
429 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
430 return false;
431 }
432
433 // How much were sections and parameters shifted due to the new parameter value?
434 qint64 diff = qint64(f.pos() - oldPos);
435
436 // Write everything back from the temporary buffer
437 tmp.seek(0);
438 if (f.write(tmp.readAll()) == -1) {
439 mErrorString = f.errorString();
440 mValid = false;
441 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
442 return false;
443 }
444 tmp.close();
445
446 // If the original file was larger than the new file, then truncate it
447 if (f.size() > f.pos()) {
448 if (!f.resize(f.pos())) {
449 mErrorString = f.errorString();
450 mValid = false;
451 EVAF_ERROR("Failed to resize the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
452 return false;
453 }
454 }
455
456 // If the shift was not zero, update the cache
457 if (diff)
458 updateCache(currentPos, diff);
459
460 // Update the parameter value in the internal cache
461 valueObject->paramValue = valueString;
462
463 }
464
465 f.close();
466
467 // Update the time when the INI file was last modified
468 QFileInfo fi(mFileName);
469 mLastModified = fi.lastModified();
470
471 return true;
472 }