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 &section) {
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 &section) {
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 &section) 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 &section) 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 &section) {
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 &section, 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 &section, 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 &section) 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 &section) {
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 &section) 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