1 /*
2 	Copyright (C) 2004-2006 Christopher E. Miller
3 	http://www.dprogramming.com/ini.php
4 
5 	This software is provided 'as-is', without any express or implied
6 	warranty.  In no event will the authors be held liable for any damages
7 	arising from the use of this software.
8 
9 	Permission is granted to anyone to use this software for any purpose,
10 	including commercial applications, and to alter it and redistribute it
11 	freely, subject to the following restrictions:
12 
13 	1. The origin of this software must not be misrepresented; you must not
14 	   claim that you wrote the original software. If you use this software
15 	   in a product, an acknowledgment in the product documentation would be
16 	   appreciated but is not required.
17 	2. Altered source versions must be plainly marked as such, and must not be
18 	   misrepresented as being the original software.
19 	3. This notice may not be removed or altered from any source distribution.
20 */
21 
22 /*
23 
24 	Modified by Jesse Phillips
25 	Made to work with D 2.0.
26 	Changed all string to string.
27 	Added some @safe and nothrow
28 	Other changes marked
29 
30 Update:
31 The Ini object no longer saves in the destructor because if it is the
32 garbage collector deleting it, some value or section object could have
33 been destructed first, resulting in undefined behavior, such as an
34 access violation. Solution: save() before you exit the program.
35 
36 
37 Portable module for reading and writing INI files of the format:
38 
39 [section]
40 key=value
41 ...
42 
43 Leading spaces and tabs are ignored.
44 Comments start with ; and should be on their own line.
45 
46 If there are comments, spaces or keys above the first section, a nameless section is created for them.
47 This means there need not be any sections in the file to have keys.
48 
49 Differences with Windows' profile (INI) functions:
50 * Windows 9x does not allow tabs in the value.
51 * Some versions do not allow the file to exceed 64 KB.
52 * If not a full file path, it's relative to the Windows directory.
53 * Windows 9x strips trailing spaces from the value.
54 * There might be a restriction on how long sections/keys/values may be.
55 * If there are double quotes around a value, Windows removes them.
56 * All key/value pairs must be in a named section.
57 
58 Проведена модификация MGW 18.02.2016
59 Отключена работа с устаревшим модулем std.stream
60 
61 */
62 
63 
64 /// Portable module for reading and writing _INI files. _ini.d version 0.6
65 module ini;
66 
67 import std.stdio; // writeln
68 
69 private import std.file, std..string; 
70 
71 
72 // debug = INI; //show file being parsed
73 
74 
75 // debug(INI)
76 // private import std.cstream;
77 
78 private class IniLine {
79 	~this() {
80 		debug(PRINT_DTORS) writeln("~IniLine\n");
81 	}
82 
83 private:
84 	string data;
85 }
86 
87 /// Key in an INI file.
88 class IniKey: IniLine {
89 protected:
90 	//these are slices in data if unmodified
91 	//if modified, data is set to null
92 	string _name;
93 	string _value;
94 
95 public:
96 	this(string name) {
97 		_name = name;
98 	}
99 
100 
101 	~this() {
102 		debug(PRINT_DTORS) writeln("~IniKey ", _name);
103 	}
104 
105 
106 // public: +++ GENA
107 	/// Property: get key _name.
108 	@property string name() { //-> имя
109 		return _name;
110 	}
111 
112 
113 	/// Property: get key _value.
114 	@property string value() { //-> значение
115 		return _value;
116 	}
117 }
118 
119 
120 /// Section of keys in an INI file.
121 class IniSection {
122 protected:
123 	Ini _ini;				// Ссылка на объект Ini
124 	string _name;			// Имя секции
125 	IniLine[] lines;		// Массив строк в секции
126 
127 	// Создание. ссылка на объект Ini и имя секции
128 	this(Ini ini, string name) {
129 		_ini = ini;			// Ссылка на объект Ini
130 		_name = name;		// Имя секции
131 	}
132 
133 	~this() {
134 		debug(PRINT_DTORS) writeln("~IniSection ", _name);
135 	}
136 
137 
138 public:
139 	/// Property: get section _name. Вернуть имя секции
140 	@property @safe nothrow
141 	string name() { //-> Вернуть/установить имя секции
142 		return _name; 
143 	}
144 
145 
146 	/// Property: set section _name. Установить имя секции. Взвести флаг модификации
147 	@property @safe nothrow
148 	void name(string newName) { //-> Вернуть/установить имя секции
149 		_ini._modified = true;
150 		_name = newName;
151 	}
152 
153 
154 	/// foreach key. Перебор ключей в цикле
155 	int opApply(int delegate(ref IniKey) dg) { //-> Найти нужный ключ
156 		int result = 0;
157 		uint i;
158 		IniKey ikey;
159 		for(i = 0; i != lines.length; i++) {
160 			ikey = cast(IniKey)lines[i];
161 			if(ikey) {
162 				result = dg(ikey);
163 				if(result)
164 					break;
165 			}
166 		}
167 		return result;
168 	}
169 
170 
171 	/// Property: get all _keys. Дай перечень всех ключей
172 	//better to use foreach unless this array is needed
173 	@property IniKey[] keys() { //-> Дай перечень всех ключей
174 		IniKey[] ikeys = new IniKey[lines.length];
175 		uint i = 0;
176 		foreach(IniKey ikey; this) {
177 			ikeys[i++] = ikey;
178 		}
179 		return ikeys[0 .. i];
180 	}
181 
182 
183 	/// Returns: _key matching keyName, or null if not present. Вернуть ключ или null если отсутствует
184 	IniKey key(string keyName) { //-> Вернуть ключ или null если отсутствует
185 		foreach(IniKey ikey; this) {
186 			if(_ini.match(ikey._name, keyName))
187 				return ikey;
188 		}
189 		return new IniKey(keyName); //didn't find it
190 	}
191 
192 
193 	/// Set an existing key's value.
194 	@safe nothrow
195 	void setValue(IniKey ikey, string newValue) { //-> Установить значение если существует
196 		ikey._value = newValue;
197 		_ini._modified = true;
198 		ikey.data = null;
199 	}
200 
201 
202 	/// Find or create key keyName and set its _value to newValue.
203 	void setValue(string keyName, string newValue) { //-> Найти или создать и установить значение
204 		IniKey ikey = key(keyName);
205 		if(!ikey.data) { // MGW Add ref on .data in ikey
206 			ikey = new IniKey(keyName);
207 			lines ~= ikey;  // К списку lines добавить ещё одну строку
208 			// _ini._modified = true; //next call does this
209 		}
210 		value(ikey, newValue);
211 	}
212 
213 
214 	/+
215 	///
216 	alias setValue value;
217 	+/
218 
219 
220 	/// Same as setValue(ikey, newValue).
221 	@safe nothrow
222 	void value(IniKey ikey, string newValue) {
223 		return setValue(ikey, newValue);
224 	}
225 
226 
227 	/// Same as setValue(keyName, newValue).
228 	void value(string keyName, string newValue) {
229 		// asm {
230 		//     int 3;
231 		// }
232 		return setValue(keyName, newValue);
233 	}
234 
235 
236 	/// Returns: value of the existing key keyName, or defaultValue if not present.
237 	string getValue(string keyName, string defaultValue = null) {
238 		foreach(IniKey ikey; this) {
239 			if(_ini.match(ikey._name, keyName))
240 				return ikey.value;
241 		}
242 		return defaultValue; //didn't find it
243 	}
244 
245 
246 	// /// Returns: _value of the existing key keyName, or null if not present.
247 	/// Same as getValue(keyName, null).
248 	string value(string keyName) { //->
249 		return getValue(keyName, null);
250 	}
251 
252 
253 	/// Shortcut for getValue(keyName).
254 	string opIndex(string keyName) { //-> Доступ по имени
255 		return value(keyName);
256 	}
257 
258 
259 	/// Shortcut for setValue(keyName, newValue).
260 	void opIndexAssign(string newValue, string keyName) { //->
261 		value(keyName, newValue);
262 	}
263 
264 
265 	/// _Remove key keyName.
266 	void remove(string keyName) { //-> Удалить ключ
267 		uint i;
268 		IniKey ikey;
269 		for(i = 0; i != lines.length; i++) {
270 			ikey = cast(IniKey)lines[i];
271 			if(ikey && _ini.match(ikey._name, keyName)) {
272 				if(i == lines.length - 1)
273 					lines = lines[0 .. i];
274 				else if(i == 0)
275 					lines = lines[1 .. lines.length];
276 				else
277 					lines = lines[0 .. i] ~ lines[i + 1 .. lines.length];
278 				_ini._modified = true;
279 				return;
280 			}
281 		}
282 	}
283 }
284 
285 
286 /// An INI file.
287 class Ini {
288 // protected:
289 	string _file;								// Имя исходного INI файла
290 	bool _modified = false;						// F - файл не изменялся
291 	IniSection[] isecs;
292 	char secStart = '[', secEnd = ']';
293 
294 	void print() {
295 		writeln("================ Class INI ===============");
296 		writeln("_file = ", _file);
297 		writeln("_modified = ", _modified);
298 		for(int i; i<isecs.length; i++) {
299 			writeln("    ", isecs[i]._name);
300 			for(int j; j<isecs[i].lines.length; j++) {
301 				IniKey ikey = cast(IniKey)isecs[i].lines[j];
302 				writeln("        ", isecs[i].lines[j].data);
303 				try {
304 					writeln("               [", ikey._name, "] --> [", ikey._value, "]  (", ikey.data,")");
305 				} catch {
306 				}
307 			}
308 		}
309 		writeln("==========================================");
310 	}
311 
312 	void parse() {
313 		debug(INI) writeln("INI parsing file ", _file);
314 		string data;
315 		int i = -1;
316 		IniSection isec;
317 		uint lineStartIndex = 0;
318 
319 		try {
320 			data = cast(string)std.file.read(_file);
321 			/+
322 			File f = new File(_file, FileMode.In);
323 			data = f.readString(f.size());
324 			delete f;
325 			+/
326 		}
327 		// catch(Throwable o)
328 		catch {
329 			debug(INI)
330 			writeln("INI no file to parse");
331 			// File f = new File(_file, "w");
332 			// write(f," ");
333 			// f.close();
334 
335 			return;
336 		}
337 		if(!data.length) {
338 			debug(INI)
339 			writeln("INI nothing to parse");
340 			return;
341 		}
342 
343 
344 		char getc() {
345 			//also increment -i- past end so ungetc works properly
346 			if(++i >= data.length)
347 				return 0;
348 			return data[i];
349 		}
350 
351 
352 		void ungetc() {
353 			assert(i > 0);
354 			i--;
355 		}
356 
357 
358 		void reset() {
359 			lineStartIndex = i + 1;
360 		}
361 
362 
363 		void eol() {
364 			IniLine iline = new IniLine;
365 			iline.data = data[lineStartIndex .. i];
366 			debug(INI) writeln("INI line: ", iline.data);
367 			isec.lines ~= iline;
368 		}
369 
370 
371 		char ch, ch2;
372 		int i2;
373 		isec = new IniSection(this, "");
374 		for(;;) {
375 			ch = getc();
376 			switch(ch) {
377 			case '\r':
378 				eol();
379 				ch2 = getc();
380 				if(ch2 != '\n')
381 					ungetc();
382 				reset();
383 				break;
384 
385 			case '\n':
386 				eol();
387 				reset();
388 				break;
389 
390 			case 0: //eof
391 ini_eof:
392 				if(lineStartIndex < i) {
393 					eol();
394 					//reset();
395 				}
396 				isecs ~= isec;
397 				if(!isecs[0].lines)
398 					isecs = isecs[1 .. isecs.length];
399 				debug(INI)
400 				writeln("INI done parsing");
401 				return;
402 
403 			case ' ':
404 			case '\t':
405 			case '\v':
406 			case '\f':
407 				break;
408 
409 			case ';': //comments
410 			case '#':
411 done_comment:
412 				for(;;) {
413 					ch2 = getc();
414 					switch(ch2) {
415 					case '\r':
416 						eol();
417 						ch2 = getc();
418 						if(ch2 != '\n')
419 							ungetc();
420 						reset();
421 						break done_comment;
422 
423 					case '\n':
424 						eol();
425 						reset();
426 						break done_comment;
427 
428 					case 0: //eof
429 						goto ini_eof;
430 
431 					default:
432 						break;
433 					}
434 				}
435 				break;
436 
437 			default:
438 				if(ch == secStart) { // '['
439 					i2 = i + 1;
440 done_sec:
441 					for(;;) {
442 						ch2 = getc();
443 						switch(ch2) {
444 						case '\r':
445 							eol();
446 							ch2 = getc();
447 							if(ch2 != '\n')
448 								ungetc();
449 							reset();
450 							break done_sec;
451 
452 						case '\n':
453 							eol();
454 							reset();
455 							break done_sec;
456 
457 						case 0: //eof
458 							goto ini_eof;
459 
460 						default:
461 							if(ch2 == secEnd) { // ']'
462 								isecs ~= isec;
463 								isec = new IniSection(this, data[i2 .. i]);
464 								debug(INI) writeln("INI section: ", isec._name);
465 								for(;;) {
466 									ch2 = getc();
467 									switch(ch2) {
468 									case ' ':
469 									case '\t':
470 									case '\v':
471 									case '\f':
472 										//ignore whitespace
473 										break;
474 
475 									case '\r':
476 										ch2 = getc();
477 										if(ch2 != '\n')
478 											ungetc();
479 										break done_sec;
480 
481 									case '\n':
482 										break done_sec;
483 
484 									default:
485 										//just treat junk after the ] as the next line
486 										ungetc();
487 										break done_sec;
488 									}
489 								}
490 								break done_sec;
491 							}
492 						}
493 					}
494 					reset();
495 					break;
496 				} else { //must be beginning of key name
497 					i2 = i;
498 done_default:
499 					for(;;) {
500 						ch2 = getc();
501 						switch(ch2) {
502 						case '\r':
503 							eol();
504 							ch2 = getc();
505 							if(ch2 != '\n')
506 								ungetc();
507 							reset();
508 							break done_default;
509 
510 						case '\n':
511 							eol();
512 							reset();
513 							break done_default;
514 
515 						case 0: //eof
516 							goto ini_eof;
517 
518 						case ' ':
519 						case '\t':
520 						case '\v':
521 						case '\f':
522 							break;
523 
524 						case '=':
525 							IniKey ikey;
526 
527 
528 							void addKey() {
529 								ikey.data = data[lineStartIndex .. i];
530 								ikey._value = data[i2 .. i];
531 								isec.lines ~= ikey;
532 								debug(INI) writeln("INI key: [", ikey._name, "] = [", ikey._value, "]");
533 							}
534 
535 
536 							ikey = new IniKey(data[i2 .. i]);
537 							i2 = i + 1; //after =
538 							for(;;) { //get key value
539 								ch2 = getc();
540 								switch(ch2) {
541 								case '\r':
542 									addKey();
543 									ch2 = getc();
544 									if(ch2 != '\n')
545 										ungetc();
546 									reset();
547 									break done_default;
548 
549 								case '\n':
550 									addKey();
551 									reset();
552 									break done_default;
553 
554 								case 0: //eof
555 									addKey();
556 									reset();
557 									goto ini_eof;
558 
559 								default:
560 									break;
561 								}
562 							}
563 							break done_default;
564 
565 						default:
566 							break;
567 						}
568 					}
569 				}
570 			}
571 		}
572 	}
573 
574 
575 	void firstOpen(string file) {
576 		//null terminated just to make it easier for the implementation
577 		//_file = toStringz(file)[0 .. file.length];
578 		// JP Modified
579 		_file = file;
580 		parse();
581 	}
582 
583 
584 public:
585 	// Added by Jesse Phillips
586 	/// Upon the next save use this file.
587 	string saveTo;
588 	// Use different section name delimiters; not recommended.
589 	this(string file, char secStart, char secEnd) {
590 		this.secStart = secStart;
591 		this.secEnd = secEnd;
592 
593 		firstOpen(file);
594 	}
595 
596 
597 	/// Construct a new INI _file.
598 	this(string file) {
599 		firstOpen(file);
600 	}
601 
602 
603 	~this() {
604 		debug(PRINT_DTORS)
605 		writeln("~Ini ", _file);
606 
607 		// The reason this is commented is explained above.
608 		/+
609 		if(_modified)
610 			save();
611 		+/
612 	}
613 
614 
615 	/// Comparison function for section and key names. Override to change behavior.
616 	bool match(string s1, string s2) {
617 		return !std..string.icmp(s1, s2);
618 	}
619 
620 
621 	//reuse same object for another file
622 	/// Open an INI _file.
623 	void open(string file) {
624 		if(_modified)
625 			save();
626 		_modified = false;
627 		isecs = null;
628 
629 		firstOpen(file);
630 	}
631 
632 
633 	/// Reload INI file; any unsaved changes are lost.
634 	void rehash() {
635 		_modified = false;
636 		isecs = null;
637 		parse();
638 	}
639 
640 
641 	/// Release memory without saving changes; contents become empty.
642 	@safe nothrow
643 	void dump() {
644 		_modified = false;
645 		isecs = null;
646 	}
647 
648 
649 	/// Property: get whether or not the INI file was _modified since it was loaded or saved.
650 	@property @safe nothrow
651 	bool modified() {
652 		return _modified;
653 	}
654 
655 
656 	/// Params:
657 	/// f = an opened-for-write stream; save() uses BufferedFile by default. Override save() to change stream.
658 	protected final void saveToStream(File f) {
659 		_modified = false;
660 
661 		// Если массив секций пустой, то выйти
662 		if(!isecs.length) return;
663 
664 		IniKey ikey;
665 		IniSection isec;
666 		uint i = 0, j;
667 
668 		if(isecs[0]._name.length)
669 			goto write_name;
670 		else //first section doesn't have a name; just keys at start of file
671 			goto after_name;
672 
673 		for(; i != isecs.length; i++) {
674 write_name:
675 			// JP Modified added dup
676 			f.writeln(secStart, isecs[i]._name, secEnd);
677 after_name:
678 			isec = isecs[i];
679 			for(j = 0; j != isec.lines.length; j++) {
680 				if(isec.lines[j].data is null) {
681 					ikey = cast(IniKey)isec.lines[j];
682 					if(ikey)
683 						ikey.data = ikey._name ~ "=" ~ ikey._value;
684 				}
685 				f.writeln(isec.lines[j].data);
686 			}
687 		}
688 	}
689 
690 	/// Write contents to disk, even if no changes were made. It is common to do if(modified)save();
691 	void save() {
692 		if(saveTo) {
693 			_file = saveTo;
694 			saveTo = null;
695 		}
696 		File f = File(_file, "w");
697 		// f.create(_file);
698 		try {
699 			saveToStream(f);
700 			f.flush();
701 		}
702 		finally {
703 			f.close();
704 		}
705 	}
706 
707 	/// Write contents to disk with filename
708 	// Added by Jesse Phillips
709 	void save(string filename) {
710 		_file = filename;
711 		save();
712 	}
713 
714 
715 	/// Finds a _section; returns null if one named name does not exist.
716 	IniSection section(string name) {
717 		foreach(IniSection isec; isecs) {
718 			if(match(isec._name, name))
719 				return isec;
720 		}
721 		return null; //didn't find it
722 	}
723 
724 
725 	/// Shortcut for section(sectionName).
726 	IniSection opIndex(string sectionName) {
727 		return section(sectionName);
728 	}
729 
730 
731 	/// The section is created if one named name does not exist.
732 	/// Returns: Section named name.
733 	IniSection addSection(string name) {
734 		IniSection isec = section(name);
735 		if(!isec) {
736 			isec = new IniSection(this, name);
737 			_modified = true;
738 			isecs ~= isec;
739 		}
740 		return isec;
741 	}
742 
743 
744 	/// foreach section.
745 	int opApply(int delegate(ref IniSection) dg) {
746 		int result = 0;
747 		foreach(IniSection isec; isecs) {
748 			result = dg(isec);
749 			if(result)
750 				break;
751 		}
752 		return result;
753 	}
754 
755 
756 	/// Property: get all _sections.
757 	@property @safe nothrow
758 	IniSection[] sections() {
759 		return isecs;
760 	}
761 
762 
763 	/// _Remove section named sectionName.
764 	void remove(string sectionName) {
765 		uint i;
766 		for(i = 0; i != isecs.length; i++) {
767 			if(match(sectionName, isecs[i]._name)) {
768 				if(i == isecs.length - 1)
769 					isecs = isecs[0 .. i];
770 				else if(i == 0)
771 					isecs = isecs[1 .. isecs.length];
772 				else
773 					isecs = isecs[0 .. i] ~ isecs[i + 1 .. isecs.length];
774 				_modified = true;
775 				return;
776 			}
777 		}
778 	}
779 }
780 
781 
782 unittest {
783 	string inifile = "unittest.ini";
784 	// Jesse Phillips
785 	// Remove file when done.
786 	scope(exit)
787 	std.file.remove(inifile);
788 	Ini ini;
789 
790 	ini = new Ini(inifile);
791 	with(ini.addSection("foo")) {
792 		value("asdf", "jkl");
793 		value("bar", "wee!");
794 		value("hi", "hello");
795 	}
796 	ini.addSection("BAR");
797 	with(ini.addSection("fOO")) {
798 		value("yes", "no");
799 	}
800 	with(ini.addSection("Hello")) {
801 		value("world", "true");
802 	}
803 	with(ini.addSection("test")) {
804 		value("1", "2");
805 		value("3", "4");
806 	}
807 	ini["test"]["value"] = "true";
808 	assert(ini["Foo"]["yes"] == "no");
809 	ini.save();
810 	delete ini;
811 
812 	ini = new Ini(inifile);
813 	assert(ini["FOO"]["Bar"] == "wee!"); //
814 	assert(ini["Foo"]["yes"] == "no");
815 	assert(ini["hello"]["world"] == "true");
816 	assert(ini["FOO"]["Bar"] == "wee!");
817 	assert(ini["55"] is null);
818 	assert(ini["hello"]["Yes"] is null);
819 
820 	ini.open(inifile);
821 	ini["bar"].remove("notta");
822 	ini["foo"].remove("bar");
823 	ini.remove("bar");
824 	assert(ini["bar"] is null);
825 	assert(ini["foo"] !is null);
826 	assert(ini["foo"]["bar"] is null);
827 	ini.remove("foo");
828 	assert(ini["foo"] is null);
829 	ini.save();
830 	delete ini;
831 }