1 /*
2 Library for inifile like data files.
3
4 Copyright (C) 2006 Olaf Klein, o.b.klein@gpsbabel.org
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 */
20
21 #include "defs.h" // for fatal, ugetenv, warning
22 #include "inifile.h"
23 #include "src/core/file.h" // for File
24 #include <QtCore/QByteArray> // for QByteArray
25 #include <QtCore/QChar> // for operator==, QChar
26 #include <QtCore/QDir> // for QDir
27 #include <QtCore/QFile> // for QFile
28 #include <QtCore/QFileInfo> // for QFileInfo
29 #include <QtCore/QHash> // for QHash
30 #include <QtCore/QIODevice> // for QIODevice::ReadOnly, QIODevice
31 #include <QtCore/QTextStream> // for QTextStream
32 #include <QtCore/Qt> // for CaseInsensitive
33 #include <QtCore/QtGlobal> // for qPrintable
34 #include <utility>
35
36 #define MYNAME "inifile"
37
38 class InifileSection
39 {
40 public:
41 QString name;
42 QHash<QString, QString> entries;
43
44 InifileSection() = default;
InifileSection(QString nm)45 explicit InifileSection(QString nm) : name{std::move(nm)} {}
46 };
47
48 /* internal procedures */
49
50 #define GPSBABEL_INIFILE "gpsbabel.ini"
51 #define GPSBABEL_SUBDIR ".gpsbabel"
52
53
54 static QString
find_gpsbabel_inifile(const QString & path)55 find_gpsbabel_inifile(const QString& path) /* can be empty or NULL */
56 {
57 if (path.isNull()) {
58 return QString();
59 }
60 QString inipath(QDir(path).filePath(GPSBABEL_INIFILE));
61 return QFile(inipath).open(QIODevice::ReadOnly) ? inipath : QString();
62 }
63
64 static QString
open_gpsbabel_inifile()65 open_gpsbabel_inifile()
66 {
67 QString res;
68
69 QString envstr = ugetenv("GPSBABELINI");
70 if (!envstr.isNull()) {
71 if (QFile(envstr).open(QIODevice::ReadOnly)) {
72 return envstr;
73 }
74 warning("WARNING: GPSBabel-inifile, defined in environment, NOT found!\n");
75 return res;
76 }
77 QString name = find_gpsbabel_inifile(""); // Check in current directory first.
78 if (name.isNull()) {
79 #ifdef __WIN32__
80 // Use &&'s early-out behaviour to try successive file locations: first
81 // %APPDATA%, then %WINDIR%, then %SYSTEMROOT%.
82 (name = find_gpsbabel_inifile(ugetenv("APPDATA"))).isNull()
83 && (name = find_gpsbabel_inifile(ugetenv("WINDIR"))).isNull()
84 && (name = find_gpsbabel_inifile(ugetenv("SYSTEMROOT"))).isNull();
85 #else
86 // Use &&'s early-out behaviour to try successive file locations: first
87 // ~/.gpsbabel, then /usr/local/etc, then /etc.
88 (name = find_gpsbabel_inifile(QDir::home().filePath(GPSBABEL_SUBDIR))).
89 isNull()
90 && (name = find_gpsbabel_inifile("/usr/local/etc")).isNull()
91 && (name = find_gpsbabel_inifile("/etc")).isNull();
92 #endif
93 }
94 if (!name.isNull()) {
95 res = name;
96 }
97 return res;
98 }
99
100 static void
inifile_load_file(QTextStream * stream,inifile_t * inifile,const char * myname)101 inifile_load_file(QTextStream* stream, inifile_t* inifile, const char* myname)
102 {
103 QString buf;
104 InifileSection section;
105
106 while (!(buf = stream->readLine()).isNull()) {
107 buf = buf.trimmed();
108
109 if (buf.isEmpty()) {
110 continue; /* skip empty lines */
111 }
112 if ((buf.at(0) == '#') || (buf.at(0) == ';')) {
113 continue; /* skip comments */
114 }
115
116 if (buf.at(0) == '[') {
117 QString section_name;
118 if (buf.contains(']')) {
119 section_name = buf.mid(1, buf.indexOf(']') - 1).trimmed();
120 }
121 if (section_name.isEmpty()) {
122 fatal("%s: invalid section header '%s' in '%s'.\n", myname, qPrintable(section_name),
123 qPrintable(inifile->source));
124 }
125
126 // form lowercase key to implement CaseInsensitive matching.
127 section_name = section_name.toLower();
128 inifile->sections.insert(section_name, InifileSection(section_name));
129 section = inifile->sections.value(section_name);
130 } else {
131 if (section.name.isEmpty()) {
132 fatal("%s: missing section header in '%s'.\n", myname,
133 qPrintable(inifile->source));
134 }
135
136 // Store key in lower case to implement CaseInsensitive matching.
137 QString key = buf.section('=', 0, 0).trimmed().toLower();
138 // Take some care so the return from inifile_find_value can
139 // be used to distinguish between a key that isn't found
140 // and a found key without a value, i.e. force value
141 // to be non-null but possibly empty.
142 QString value = buf.section('=', 1).append("").trimmed();
143 section.entries.insert(key, value);
144
145 // update the QHash sections with the modified InifileSection section.
146 inifile->sections.insert(section.name, section);
147 }
148 }
149 }
150
151 static const QString
inifile_find_value(const inifile_t * inifile,const QString & sec_name,const QString & key)152 inifile_find_value(const inifile_t* inifile, const QString& sec_name, const QString& key)
153 {
154 if (inifile == nullptr) {
155 return QString();
156 }
157
158 // CaseInsensitive matching implemented by forcing sec_name & key to lower case.
159 return inifile->sections.value(sec_name.toLower()).entries.value(key.toLower());
160 }
161
162 /* public procedures */
163
164 /*
165 inifile_init:
166 reads inifile filename into memory
167 myname represents the calling module
168
169 filename == NULL: try to open global gpsbabel.ini
170 */
171 inifile_t*
inifile_init(const QString & filename,const char * myname)172 inifile_init(const QString& filename, const char* myname)
173 {
174 QString name;
175
176 if (filename.isEmpty()) {
177 name = open_gpsbabel_inifile();
178 if (name.isEmpty()) {
179 return nullptr;
180 }
181 } else {
182 name = filename;
183 }
184
185 gpsbabel::File file(name);
186 file.open(QFile::ReadOnly);
187 QTextStream stream(&file);
188 stream.setCodec("UTF-8");
189 stream.setAutoDetectUnicode(true);
190
191 auto* result = new inifile_t;
192 QFileInfo fileinfo(file);
193 result->source = fileinfo.absoluteFilePath();
194 inifile_load_file(&stream, result, myname);
195
196 file.close();
197 return result;
198 }
199
200 void
inifile_done(inifile_t * inifile)201 inifile_done(inifile_t* inifile)
202 {
203 delete inifile;
204 }
205
206 bool
inifile_has_section(const inifile_t * inifile,const QString & section)207 inifile_has_section(const inifile_t* inifile, const QString& section)
208 {
209 return inifile->sections.contains(section.toLower());
210 }
211
212 /*
213 inifile_readstr:
214 returns a null QString if not found, otherwise a non-null but possibly
215 empty Qstring with the value of key ...
216 */
217
218 QString
inifile_readstr(const inifile_t * inifile,const QString & section,const QString & key)219 inifile_readstr(const inifile_t* inifile, const QString& section, const QString& key)
220 {
221 return inifile_find_value(inifile, section, key);
222 }
223
224 /*
225 inifile_readint:
226 on success the value is stored into "*value" and "inifile_readint" returns 1,
227 otherwise inifile_readint returns 0
228 */
229
230 int
inifile_readint(const inifile_t * inifile,const QString & section,const QString & key,int * value)231 inifile_readint(const inifile_t* inifile, const QString& section, const QString& key, int* value)
232 {
233 const QString str = inifile_find_value(inifile, section, key);
234
235 if (str.isNull()) {
236 return 0;
237 }
238
239 if (value != nullptr) {
240 *value = str.toInt();
241 }
242 return 1;
243 }
244
245 /*
246 inifile_readint_def:
247 if found inifile_readint_def returns value of key, otherwise a default value "def"
248 */
249
250 int
inifile_readint_def(const inifile_t * inifile,const QString & section,const QString & key,const int def)251 inifile_readint_def(const inifile_t* inifile, const QString& section, const QString& key, const int def)
252 {
253 int result;
254
255 if (inifile_readint(inifile, section, key, &result) == 0) {
256 return def;
257 } else {
258 return result;
259 }
260 }
261
262