]> vaikene.ee Git - evaf/blob - src/libs/Common/inifile.cpp
Warning fixes and copyright update.
[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-2019 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 : d(new Internal::IniFileImpl(fileName, mode))
41 {
42 }
43
44 IniFile::~IniFile()
45 {
46 d.reset();
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(QByteArray const & paramName, QVariant const & defaultValue)
60 {
61 return d->getValue(paramName, defaultValue);
62 }
63
64 bool IniFile::setValue(QByteArray 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(qint64 pos, qint64 diff)
100 {
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;
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<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;
116 }
117 }
118 }
119
120 QExplicitlySharedDataPointer<IniFileSection> IniFileImpl::getSection(QFile & file, QByteArray 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<QByteArray, 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 QByteArray 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 (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);
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, QByteArray const & paramName)
170 {
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
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 qint64 currentPos = file.pos();
185
186 QByteArray 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 QByteArray name = line.mid(0, idx).trimmed().toLower();
206 QByteArray value = line.mid(idx + 1).trimmed();
207
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:"))
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<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);
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 (qstricmp(name.constData(), paramName.constData()) == 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(QByteArray 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 QByteArray section = paramName.left(idx);
268 QByteArray 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 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;
295
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('\"')) {
301 v.remove(0, 1);
302 if (v.endsWith('\"'))
303 v.remove(v.size() - 1, 1);
304 }
305 else if (v.startsWith('\'')) {
306 v.remove(0, 1);
307 if (v.endsWith('\''))
308 v.remove(v.size() - 1, 1);
309 }
310
311 // Convert from the escaped character array
312 if (defaultValue.type() == QVariant::String)
313 valueObject->value = QVariant(strFromEscapedCharArray(v));
314 else
315 valueObject->value = QVariant(binFromEscapedCharArray(v));
316 }
317 else
318 valueObject->value = toVariant(valueObject->paramValue, defaultValue);
319
320 return valueObject->value;
321 }
322
323 bool IniFileImpl::setValue(QByteArray const & paramName, QVariant const & value)
324 {
325 // Locate the '/' character that separates section names from key names
326 int idx = paramName.lastIndexOf('/', -1);
327 if (idx < 0)
328 return false;
329
330 // Separate section and key names
331 QByteArray section = paramName.left(idx).toLower();
332 QByteArray key = paramName.mid(idx + 1).toLower();
333
334 // Format the value depending on the type
335 QByteArray valueString;
336 switch (value.type()) {
337 case QVariant::UInt:
338 valueString = QByteArray("0x").append(QByteArray::number(value.toUInt(), 16));
339 break;
340 case QVariant::Int:
341 valueString = QByteArray::number(value.toInt());
342 break;
343 case QVariant::Double:
344 valueString = QByteArray::number(value.toDouble(), 'f');
345 break;
346 case QVariant::Bool:
347 valueString = value.toBool() ? "true" : "false";
348 break;
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));
354 else
355 valueString = QByteArray(1, static_cast<char const>(c.unicode()));
356 break;
357 }
358 case QVariant::ByteArray:
359 valueString = binToEscapedCharArray(value.toByteArray());
360 if (valueString.startsWith(' ') || valueString.endsWith(' ')) {
361 valueString.insert(0, '\"');
362 valueString.append('\"');
363 }
364 break;
365 case QVariant::String:
366 valueString = strToEscapedCharArray(value.toString());
367 if (valueString.startsWith(' ') || valueString.endsWith(' ')) {
368 valueString.insert(0, '\"');
369 valueString.append('\"');
370 }
371 break;
372 default:
373 valueString = value.toString().toLatin1();
374 }
375
376 // Open the file
377 QFile f(mFileName);
378 if (!f.open(mMode)) {
379 mErrorString = f.errorString();
380 mValid = false;
381 EVAF_ERROR("Failed to open the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
382 return false;
383 }
384 mValid = true;
385 mErrorString.clear();
386
387 // Get the section object
388 QExplicitlySharedDataPointer<IniFileSection> sectionObject = getSection(f, section);
389
390 // If the section is not found, add a new section to the end of the INI file
391 if (!sectionObject) {
392
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();
396 mValid = false;
397 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
398 return false;
399 }
400
401 // Current file position
402 qint64 currentPos = f.pos();
403
404 // Add the new section to the internal cache
405 sectionObject = new IniFileSection(currentPos);
406 mCache.insert(section.toLower(), sectionObject);
407
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();
411 mValid = false;
412 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
413 return false;
414 }
415
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);
422 }
423
424 // If the section is found, use the existing section object from the cache
425 else {
426 qint64 currentPos;
427 qint64 oldPos = f.pos();
428 QString prefix; // Platform-specific prefix
429
430 // Locate the parameter value
431 QExplicitlySharedDataPointer<IniFileValue> valueObject = getParameter(f, *sectionObject, key);
432
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();
435
436 if (valueObject) {
437 // Parameter was found; skip the line with the current parameter value
438 f.readLine();
439 oldPos = f.pos();
440
441 // Format the prefix string if necessary
442 if (valueObject->thisOsOnly) {
443 #ifdef Q_OS_LINUX
444 prefix = "linux:";
445 #endif
446 #ifdef Q_OS_WIN32
447 prefix = "windows:";
448 #endif
449 }
450 }
451
452 else {
453 // Parameter was not found; create a new value object
454 valueObject = new IniFileValue(currentPos);
455 valueObject->name = key;
456 }
457
458 // Store everything from the current position till the end of file in a temporary buffer
459 QBuffer tmp;
460 if (!tmp.open(QBuffer::ReadWrite)) {
461 mErrorString = tmp.errorString();
462 mValid = false;
463 EVAF_ERROR("Failed to open the temporary buffer : %s", qPrintable(mErrorString));
464 return false;
465 }
466 if (tmp.write(f.readAll()) == -1) {
467 mErrorString = tmp.errorString();
468 mValid = false;
469 EVAF_ERROR("Failed to write to the temporary buffer : %s", qPrintable(mErrorString));
470 return false;
471 }
472
473 // Rewind to the original position and write the new parameter value
474 f.seek(currentPos);
475 if (f.write(QString("%1%2 = %3" EOL).arg(prefix).arg(key.constData()).arg(valueString.constData()).toLatin1()) == -1) {
476 mErrorString = f.errorString();
477 mValid = false;
478 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
479 return false;
480 }
481
482 // How much were sections and parameters shifted due to the new parameter value?
483 qint64 diff = qint64(f.pos() - oldPos);
484
485 // Write everything back from the temporary buffer
486 tmp.seek(0);
487 if (f.write(tmp.readAll()) == -1) {
488 mErrorString = f.errorString();
489 mValid = false;
490 EVAF_ERROR("Failed to write to the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
491 return false;
492 }
493 tmp.close();
494
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();
499 mValid = false;
500 EVAF_ERROR("Failed to resize the INI file %s : %s", qPrintable(mFileName), qPrintable(mErrorString));
501 return false;
502 }
503 }
504
505 // If the shift was not zero, update the cache
506 if (diff)
507 updateCache(currentPos, diff);
508
509 // Update the parameter value in the internal cache
510 valueObject->paramValue = valueString;
511 valueObject->value = value;
512
513 }
514
515 f.close();
516
517 // Update the time when the INI file was last modified
518 QFileInfo fi(mFileName);
519 mLastModified = fi.lastModified();
520
521 return true;
522 }