1 /* ScummVM - Graphic Adventure Engine
2 *
3 * ScummVM is the legal property of its developers, whose names
4 * are too numerous to list here. Please refer to the COPYRIGHT
5 * file distributed with this source distribution.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23 #include "common/ini-file.h"
24 #include "common/file.h"
25 #include "common/savefile.h"
26 #include "common/system.h"
27 #include "common/textconsole.h"
28
29 namespace Common {
30
isValidName(const String & name)31 bool INIFile::isValidName(const String &name) {
32 const char *p = name.c_str();
33 while (*p && (isAlnum(*p) || *p == '-' || *p == '_' || *p == '.'))
34 p++;
35 return *p == 0;
36 }
37
INIFile()38 INIFile::INIFile() {
39 }
40
~INIFile()41 INIFile::~INIFile() {
42 }
43
clear()44 void INIFile::clear() {
45 _sections.clear();
46 }
47
loadFromFile(const String & filename)48 bool INIFile::loadFromFile(const String &filename) {
49 File file;
50 if (file.open(filename))
51 return loadFromStream(file);
52 else
53 return false;
54 }
55
loadFromSaveFile(const String & filename)56 bool INIFile::loadFromSaveFile(const String &filename) {
57 assert(g_system);
58 SaveFileManager *saveFileMan = g_system->getSavefileManager();
59 SeekableReadStream *loadFile;
60
61 assert(saveFileMan);
62 if (!(loadFile = saveFileMan->openForLoading(filename)))
63 return false;
64
65 bool status = loadFromStream(*loadFile);
66 delete loadFile;
67 return status;
68 }
69
loadFromStream(SeekableReadStream & stream)70 bool INIFile::loadFromStream(SeekableReadStream &stream) {
71 Section section;
72 KeyValue kv;
73 String comment;
74 int lineno = 0;
75
76 // TODO: Detect if a section occurs multiple times (or likewise, if
77 // a key occurs multiple times inside one section).
78
79 while (!stream.eos() && !stream.err()) {
80 lineno++;
81
82 // Read a line
83 String line = stream.readLine();
84
85 if (line.size() == 0) {
86 // Do nothing
87 } else if (line[0] == '#' || line[0] == ';' || line.hasPrefix("//")) {
88 // Accumulate comments here. Once we encounter either the start
89 // of a new section, or a key-value-pair, we associate the value
90 // of the 'comment' variable with that entity. The semicolon and
91 // C++-style comments are used for Living Books games in Mohawk.
92 comment += line;
93 comment += "\n";
94 } else if (line[0] == '(') {
95 // HACK: The following is a hack added by Kirben to support the
96 // "map.ini" used in the HE SCUMM game "SPY Fox in Hold the Mustard".
97 //
98 // It would be nice if this hack could be restricted to that game,
99 // but the current design of this class doesn't allow to do that
100 // in a nice fashion (a "isMustard" parameter is *not* a nice
101 // solution).
102 comment += line;
103 comment += "\n";
104 } else if (line[0] == '[') {
105 // It's a new section which begins here.
106 const char *p = line.c_str() + 1;
107 // Get the section name, and check whether it's valid (that
108 // is, verify that it only consists of alphanumerics,
109 // periods, dashes and underscores). Mohawk Living Books games
110 // can have periods in their section names.
111 while (*p && (isAlnum(*p) || *p == '-' || *p == '_' || *p == '.'))
112 p++;
113
114 if (*p == '\0')
115 error("INIFile::loadFromStream: missing ] in line %d", lineno);
116 else if (*p != ']')
117 error("INIFile::loadFromStream: Invalid character '%c' occurred in section name in line %d", *p, lineno);
118
119 // Previous section is finished now, store it.
120 if (!section.name.empty())
121 _sections.push_back(section);
122
123 section.name = String(line.c_str() + 1, p);
124 section.keys.clear();
125 section.comment = comment;
126 comment.clear();
127
128 assert(isValidName(section.name));
129 } else {
130 // This line should be a line with a 'key=value' pair, or an empty one.
131
132 // Skip leading whitespaces
133 const char *t = line.c_str();
134 while (isSpace(*t))
135 t++;
136
137 // Skip empty lines / lines with only whitespace
138 if (*t == 0)
139 continue;
140
141 // If no section has been set, this config file is invalid!
142 if (section.name.empty()) {
143 error("INIFile::loadFromStream: Key/value pair found outside a section in line %d", lineno);
144 }
145
146 // Split string at '=' into 'key' and 'value'. First, find the "=" delimeter.
147 const char *p = strchr(t, '=');
148 if (!p)
149 error("Config file buggy: Junk found in line line %d: '%s'", lineno, t);
150
151 // Extract the key/value pair
152 kv.key = String(t, p);
153 kv.value = String(p + 1);
154
155 // Trim of spaces
156 kv.key.trim();
157 kv.value.trim();
158
159 // Store comment
160 kv.comment = comment;
161 comment.clear();
162
163 assert(isValidName(kv.key));
164
165 section.keys.push_back(kv);
166 }
167 }
168
169 // Save last section
170 if (!section.name.empty())
171 _sections.push_back(section);
172
173 return (!stream.err() || stream.eos());
174 }
175
saveToFile(const String & filename)176 bool INIFile::saveToFile(const String &filename) {
177 DumpFile file;
178 if (file.open(filename))
179 return saveToStream(file);
180 else
181 return false;
182 }
183
saveToSaveFile(const String & filename)184 bool INIFile::saveToSaveFile(const String &filename) {
185 assert(g_system);
186 SaveFileManager *saveFileMan = g_system->getSavefileManager();
187 WriteStream *saveFile;
188
189 assert(saveFileMan);
190 if (!(saveFile = saveFileMan->openForSaving(filename)))
191 return false;
192
193 bool status = saveToStream(*saveFile);
194 delete saveFile;
195 return status;
196 }
197
saveToStream(WriteStream & stream)198 bool INIFile::saveToStream(WriteStream &stream) {
199 for (List<Section>::iterator i = _sections.begin(); i != _sections.end(); ++i) {
200 // Write out the section comment, if any
201 if (! i->comment.empty()) {
202 stream.writeString(i->comment);
203 }
204
205 // Write out the section name
206 stream.writeByte('[');
207 stream.writeString(i->name);
208 stream.writeByte(']');
209 stream.writeByte('\n');
210
211 // Write out the key/value pairs
212 for (List<KeyValue>::iterator kv = i->keys.begin(); kv != i->keys.end(); ++kv) {
213 // Write out the comment, if any
214 if (! kv->comment.empty()) {
215 stream.writeString(kv->comment);
216 }
217 // Write out the key/value pair
218 stream.writeString(kv->key);
219 stream.writeByte('=');
220 stream.writeString(kv->value);
221 stream.writeByte('\n');
222 }
223 }
224
225 stream.flush();
226 return !stream.err();
227 }
228
addSection(const String & section)229 void INIFile::addSection(const String §ion) {
230 Section *s = getSection(section);
231 if (s)
232 return;
233
234 Section newSection;
235 newSection.name = section;
236 _sections.push_back(newSection);
237 }
238
removeSection(const String & section)239 void INIFile::removeSection(const String §ion) {
240 assert(isValidName(section));
241 for (List<Section>::iterator i = _sections.begin(); i != _sections.end(); ++i) {
242 if (section.equalsIgnoreCase(i->name)) {
243 _sections.erase(i);
244 return;
245 }
246 }
247 }
248
hasSection(const String & section) const249 bool INIFile::hasSection(const String §ion) const {
250 assert(isValidName(section));
251 const Section *s = getSection(section);
252 return s != 0;
253 }
254
renameSection(const String & oldName,const String & newName)255 void INIFile::renameSection(const String &oldName, const String &newName) {
256 assert(isValidName(oldName));
257 assert(isValidName(newName));
258
259 Section *os = getSection(oldName);
260 const Section *ns = getSection(newName);
261 if (os) {
262 // HACK: For now we just print a warning, for more info see the TODO
263 // below.
264 if (ns)
265 warning("INIFile::renameSection: Section name \"%s\" already used", newName.c_str());
266 else
267 os->name = newName;
268 }
269 // TODO: Check here whether there already is a section with the
270 // new name. Not sure how to cope with that case, we could:
271 // - simply remove the existing "newName" section
272 // - error out
273 // - merge the two sections "oldName" and "newName"
274 }
275
276
hasKey(const String & key,const String & section) const277 bool INIFile::hasKey(const String &key, const String §ion) const {
278 assert(isValidName(key));
279 assert(isValidName(section));
280
281 const Section *s = getSection(section);
282 if (!s)
283 return false;
284 return s->hasKey(key);
285 }
286
removeKey(const String & key,const String & section)287 void INIFile::removeKey(const String &key, const String §ion) {
288 assert(isValidName(key));
289 assert(isValidName(section));
290
291 Section *s = getSection(section);
292 if (s)
293 s->removeKey(key);
294 }
295
getKey(const String & key,const String & section,String & value) const296 bool INIFile::getKey(const String &key, const String §ion, String &value) const {
297 assert(isValidName(key));
298 assert(isValidName(section));
299
300 const Section *s = getSection(section);
301 if (!s)
302 return false;
303 const KeyValue *kv = s->getKey(key);
304 if (!kv)
305 return false;
306 value = kv->value;
307 return true;
308 }
309
setKey(const String & key,const String & section,const String & value)310 void INIFile::setKey(const String &key, const String §ion, const String &value) {
311 assert(isValidName(key));
312 assert(isValidName(section));
313 // TODO: Verify that value is valid, too. In particular, it shouldn't
314 // contain CR or LF...
315
316 Section *s = getSection(section);
317 if (!s) {
318 KeyValue newKV;
319 newKV.key = key;
320 newKV.value = value;
321
322 Section newSection;
323 newSection.name = section;
324 newSection.keys.push_back(newKV);
325
326 _sections.push_back(newSection);
327 } else {
328 s->setKey(key, value);
329 }
330 }
331
getKeys(const String & section) const332 const INIFile::SectionKeyList INIFile::getKeys(const String §ion) const {
333 const Section *s = getSection(section);
334
335 return s->getKeys();
336 }
337
getSection(const String & section)338 INIFile::Section *INIFile::getSection(const String §ion) {
339 for (List<Section>::iterator i = _sections.begin(); i != _sections.end(); ++i) {
340 if (section.equalsIgnoreCase(i->name)) {
341 return &(*i);
342 }
343 }
344 return 0;
345 }
346
getSection(const String & section) const347 const INIFile::Section *INIFile::getSection(const String §ion) const {
348 for (List<Section>::const_iterator i = _sections.begin(); i != _sections.end(); ++i) {
349 if (section.equalsIgnoreCase(i->name)) {
350 return &(*i);
351 }
352 }
353 return 0;
354 }
355
hasKey(const String & key) const356 bool INIFile::Section::hasKey(const String &key) const {
357 return getKey(key) != 0;
358 }
359
getKey(const String & key) const360 const INIFile::KeyValue* INIFile::Section::getKey(const String &key) const {
361 for (List<KeyValue>::const_iterator i = keys.begin(); i != keys.end(); ++i) {
362 if (key.equalsIgnoreCase(i->key)) {
363 return &(*i);
364 }
365 }
366 return 0;
367 }
368
setKey(const String & key,const String & value)369 void INIFile::Section::setKey(const String &key, const String &value) {
370 for (List<KeyValue>::iterator i = keys.begin(); i != keys.end(); ++i) {
371 if (key.equalsIgnoreCase(i->key)) {
372 i->value = value;
373 return;
374 }
375 }
376
377 KeyValue newKV;
378 newKV.key = key;
379 newKV.value = value;
380 keys.push_back(newKV);
381 }
382
removeKey(const String & key)383 void INIFile::Section::removeKey(const String &key) {
384 for (List<KeyValue>::iterator i = keys.begin(); i != keys.end(); ++i) {
385 if (key.equalsIgnoreCase(i->key)) {
386 keys.erase(i);
387 return;
388 }
389 }
390 }
391
392 } // End of namespace Common
393