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