1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied.  See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 module hunt.shiro.config.Ini;
20 
21 import hunt.collection;
22 import hunt.Exceptions;
23 import hunt.logging.Logger;
24 import hunt.Object;
25 import hunt.util.StringBuilder;
26 import hunt.util.ObjectUtils;
27 
28 import std.array;
29 import std.file;
30 import std.path;
31 import std.range;
32 import std.string;
33 import std.stdio;
34 import std.ascii;
35 
36 
37 /**
38  * A class representing the <a href="http://en.wikipedia.org/wiki/INI_file">INI</a> text configuration format.
39  * <p/>
40  * An Ini instance is a map of {@link IniSection IniSection}s, keyed by section name.  Each
41  * {@code IniSection} is itself a map of {@code string} name/value pairs.  Name/value pairs are guaranteed to be unique
42  * within each {@code IniSection} only - not across the entire {@code Ini} instance.
43  *
44  * @since 1.0
45  */
46 class Ini : Map!(string, IniSection) {
47     
48     enum string DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed section
49     enum string DEFAULT_CHARSET_NAME = "UTF-8";
50 
51     enum string COMMENT_POUND = "#";
52     enum string COMMENT_SEMICOLON = ";";
53     enum string SECTION_PREFIX = "[";
54     enum string SECTION_SUFFIX = "]";
55 
56     private Map!(string, IniSection) sections;
57 
58     /**
59      * Creates a new empty {@code Ini} instance.
60      */
61     this() {
62         this.sections = new LinkedHashMap!(string, IniSection)();
63     }
64 
65     /**
66      * Creates a new {@code Ini} instance with the specified defaults.
67      *
68      * @param defaults the default sections and/or key-value pairs to copy into the new instance.
69      */
70     this(Ini defaults) {
71         this();
72         if (defaults is null) {
73             throw new NullPointerException("Defaults cannot be null.");
74         }
75         foreach (IniSection section ; defaults.getSections()) {
76             IniSection copy = new IniSection(section);
77             this.sections.put(section.getName(), copy);
78         }
79     }
80 
81     /**
82      * Returns {@code true} if no sections have been configured, or if there are sections, but the sections themselves
83      * are all empty, {@code false} otherwise.
84      *
85      * @return {@code true} if no sections have been configured, or if there are sections, but the sections themselves
86      *         are all empty, {@code false} otherwise.
87      */
88     bool isEmpty() {
89         IniSection[] sections = this.sections.values();
90         foreach (IniSection section ; sections) {
91             if (!section.isEmpty()) {
92                 return false;
93             }
94         }
95         return true;
96     }
97 
98     /**
99      * Returns the names of all sections managed by this {@code Ini} instance or an empty collection if there are
100      * no sections.
101      *
102      * @return the names of all sections managed by this {@code Ini} instance or an empty collection if there are
103      *         no sections.
104      */
105     string[] getSectionNames() {
106         return sections.byKey.array();
107     }
108 
109     /**
110      * Returns the sections managed by this {@code Ini} instance or an empty collection if there are
111      * no sections.
112      *
113      * @return the sections managed by this {@code Ini} instance or an empty collection if there are
114      *         no sections.
115      */
116     IniSection[] getSections() {
117         return sections.values();
118     }
119 
120     /**
121      * Returns the {@link IniSection} with the given name or {@code null} if no section with that name exists.
122      *
123      * @param sectionName the name of the section to retrieve.
124      * @return the {@link IniSection} with the given name or {@code null} if no section with that name exists.
125      */
126     IniSection getSection(string sectionName) {
127         string name = cleanName(sectionName);
128         return sections.get(name);
129     }
130 
131     /**
132      * Ensures a section with the specified name exists, adding a new one if it does not yet exist.
133      *
134      * @param sectionName the name of the section to ensure existence
135      * @return the section created if it did not yet exist, or the existing IniSection that already existed.
136      */
137     IniSection addSection(string sectionName) {
138         string name = cleanName(sectionName);
139         IniSection section = getSection(name);
140         if (section is null) {
141             section = new IniSection(name);
142             this.sections.put(name, section);
143         }
144         return section;
145     }
146 
147     /**
148      * Removes the section with the specified name and returns it, or {@code null} if the section did not exist.
149      *
150      * @param sectionName the name of the section to remove.
151      * @return the section with the specified name or {@code null} if the section did not exist.
152      */
153     IniSection removeSection(string sectionName) {
154         string name = cleanName(sectionName);
155         return this.sections.remove(name);
156     }
157 
158     private static string cleanName(string sectionName) {
159         string name = strip(sectionName);
160         if (name.empty) {
161             trace("Specified name was null or empty.  Defaulting to the default section (name = \"\")");
162             name = DEFAULT_SECTION_NAME;
163         }
164         return name;
165     }
166 
167     /**
168      * Sets a name/value pair for the section with the given {@code sectionName}.  If the section does not yet exist,
169      * it will be created.  If the {@code sectionName} is null or empty, the name/value pair will be placed in the
170      * default (unnamed, empty string) section.
171      *
172      * @param sectionName   the name of the section to add the name/value pair
173      * @param propertyName  the name of the property to add
174      * @param propertyValue the property value
175      */
176     void setSectionProperty(string sectionName, string propertyName, string propertyValue) {
177         string name = cleanName(sectionName);
178         IniSection section = getSection(name);
179         if (section is null) {
180             section = addSection(name);
181         }
182         section.put(propertyName, propertyValue);
183     }
184 
185     /**
186      * Returns the value of the specified section property, or {@code null} if the section or property do not exist.
187      *
188      * @param sectionName  the name of the section to retrieve to acquire the property value
189      * @param propertyName the name of the section property for which to return the value
190      * @return the value of the specified section property, or {@code null} if the section or property do not exist.
191      */
192     string getSectionProperty(string sectionName, string propertyName) {
193         IniSection section = getSection(sectionName);
194         return section !is null ? section.get(propertyName) : null;
195     }
196 
197     /**
198      * Returns the value of the specified section property, or the {@code defaultValue} if the section or
199      * property do not exist.
200      *
201      * @param sectionName  the name of the section to add the name/value pair
202      * @param propertyName the name of the property to add
203      * @param defaultValue the default value to return if the section or property do not exist.
204      * @return the value of the specified section property, or the {@code defaultValue} if the section or
205      *         property do not exist.
206      */
207     string getSectionProperty(string sectionName, string propertyName, string defaultValue) {
208         string value = getSectionProperty(sectionName, propertyName);
209         return value !is null ? value : defaultValue;
210     }
211 
212     /**
213      * Creates a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path.  The
214      * resource path may be any value interpretable by the
215      * {@link ResourceUtils#getInputStreamForPath(string) ResourceUtils.getInputStreamForPath} method.
216      *
217      * @param resourcePath the resource location of the INI data to load when creating the {@code Ini} instance.
218      * @return a new {@code Ini} instance loaded with the INI-formatted data in the resource at the given path.
219      * @throws ConfigurationException if the path cannot be loaded into an {@code Ini} instance.
220      */
221     static Ini fromResourcePath(string resourcePath) {
222         if (resourcePath.empty()) {
223             throw new IllegalArgumentException("Resource Path argument cannot be null or empty.");
224         }
225         Ini ini = new Ini();
226         ini.loadFromPath(resourcePath);
227         return ini;
228     }
229 
230     /**
231      * Loads data from the specified resource path into this current {@code Ini} instance.  The
232      * resource path may be any value interpretable by the
233      * {@link ResourceUtils#getInputStreamForPath(string) ResourceUtils.getInputStreamForPath} method.
234      *
235      * @param resourcePath the resource location of the INI data to load into this instance.
236      * @throws ConfigurationException if the path cannot be loaded
237      */
238     void loadFromPath(string resourcePath) {
239         string rootPath = dirName(thisExePath());
240         string filename = buildPath(rootPath, resourcePath);
241         if (!exists(filename) || isDir(filename)) {
242             throw new ConfigurationException("The config file doesn't exist: " ~ filename);
243         }
244 
245         File f = File(filename, "r");
246         if (!f.isOpen())
247             return;
248         scope (exit)
249             f.close();
250             
251         // https://dlang.org/phobos/std_stdio.html#byLine
252         parsing!(typeof(f.byLine()), true)(f.byLine());
253     }
254 
255     /**
256      * Loads the specified raw INI-formatted text into this instance.
257      *
258      * @param iniConfig the raw INI-formatted text to load into this instance.
259      * @throws ConfigurationException if the text cannot be loaded
260      */
261     void load(string iniConfig) {
262         parsing(iniConfig.lineSplitter());
263     }
264 
265     private void parsing(T, bool needDup=false)(T lines) {
266         // trace(typeid(T));
267         
268         string sectionName = DEFAULT_SECTION_NAME;
269         StringBuilder sectionContent = new StringBuilder();
270 
271         while (!lines.empty()) {
272             string rawLine = cast(string)lines.front();
273             // version(HUNT_DEBUG_CONFIG) trace(rawLine);
274             scope(exit)
275                 lines.popFront();
276             string line = strip(rawLine);
277 
278             if (line.empty() || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {
279                 //skip empty lines and comments:
280                 continue;
281             }
282 
283             static if(needDup) {
284                 line = line.idup();
285             }
286 
287             string newSectionName = getSectionName(line);
288             if (!newSectionName.empty()) {
289                 // infof("sectionName=%s, newSectionName=%s", sectionName, newSectionName);
290                 //found a new section - convert the currently buffered one into a IniSection object
291                 addSection(sectionName, sectionContent);
292 
293                 //reset the buffer for the new section:
294                 sectionContent = new StringBuilder();
295                 sectionName = newSectionName;
296 
297                 version(HUNT_DEBUG) {
298                     trace("Parsing " ~ SECTION_PREFIX ~ sectionName ~ SECTION_SUFFIX);
299                 }
300             } else {
301                 //normal line - add it to the existing content buffer:
302                 sectionContent.append(rawLine).append("\n");
303             }
304         }
305 
306         
307         //finish any remaining buffered content:
308         addSection(sectionName, sectionContent);
309     }
310 
311     /**
312      * Loads the INI-formatted text backed by the given InputStream into this instance.  This implementation will
313      * close the input stream after it has finished loading.  It is expected that the stream's contents are
314      * UTF-8 encoded.
315      *
316      * @param is the {@code InputStream} from which to read the INI-formatted text
317      * @throws ConfigurationException if unable
318      */
319     // void load(InputStream is) {
320     //     if (is is null) {
321     //         throw new NullPointerException("InputStream argument cannot be null.");
322     //     }
323     //     InputStreamReader isr;
324     //     try {
325     //         isr = new InputStreamReader(is, DEFAULT_CHARSET_NAME);
326     //     } catch (UnsupportedEncodingException e) {
327     //         throw new ConfigurationException(e);
328     //     }
329     //     load(isr);
330     // }
331 
332     /**
333      * Loads the INI-formatted text backed by the given Reader into this instance.  This implementation will close the
334      * reader after it has finished loading.
335      *
336      * @param reader the {@code Reader} from which to read the INI-formatted text
337      */
338     // void load(Reader reader) {
339     //     Scanner scanner = new Scanner(reader);
340     //     try {
341     //         load(scanner);
342     //     } finally {
343     //         try {
344     //             scanner.close();
345     //         } catch (Exception e) {
346     //             trace("Unable to cleanly close the InputStream scanner.  Non-critical - ignoring.", e);
347     //         }
348     //     }
349     // }
350 
351     /**
352      * Merges the contents of <code>m</code>'s {@link IniSection} objects into self.
353      * This differs from {@link Ini#putAll(Map)}, in that each section is merged with the existing one.
354      * For example the following two ini blocks are merged and the result is the third<BR/>
355      * <p>
356      * Initial:
357      * <pre>
358      * <code>[section1]
359      * key1 = value1
360      *
361      * [section2]
362      * key2 = value2
363      * </code> </pre>
364      *
365      * To be merged:
366      * <pre>
367      * <code>[section1]
368      * foo = bar
369      *
370      * [section2]
371      * key2 = new value
372      * </code> </pre>
373      *
374      * Result:
375      * <pre>
376      * <code>[section1]
377      * key1 = value1
378      * foo = bar
379      *
380      * [section2]
381      * key2 = new value
382      * </code> </pre>
383      *
384      * </p>
385      *
386      * @param m map to be merged
387      * @since 1.4
388      */
389     void merge(Map!(string, IniSection) m) {
390 
391         if (m !is null) {
392             foreach (string key, IniSection value; m) {
393                 IniSection section = this.getSection(key);
394                 if (section is null) {
395                     section = addSection(key);
396                 }
397                 section.putAll(value);
398             }
399         }
400     }
401 
402     private void addSection(string name, StringBuilder content) {
403         if (content.length() > 0) {
404             string contentString = content.toString();
405             string cleaned = strip(contentString);
406             if (!cleaned.empty) {
407                 IniSection section = new IniSection(name, contentString);
408                 if (!section.isEmpty()) {
409                     sections.put(name, section);
410                 }
411             }
412         }
413     }
414 
415     protected static bool isSectionHeader(string line) {
416         string s = strip(line);
417         return !s.empty && s.startsWith(SECTION_PREFIX) && s.endsWith(SECTION_SUFFIX);
418     }
419 
420     protected static string getSectionName(string line) {
421         string s = strip(line);
422         if (isSectionHeader(s)) {
423             return cleanName(s[1 .. $ - 1]);
424         }
425         return null;
426     }
427 
428 
429     bool replace(string key, IniSection oldValue, IniSection newValue) {
430         IniSection curValue = get(key);
431         if (curValue != oldValue || !containsKey(key)) {
432             return false;
433         }
434         put(key, newValue);
435         return true;
436     }
437 
438     IniSection replace(string key, IniSection value) {
439         IniSection curValue = IniSection.init;
440         if (containsKey(key)) {
441             curValue = put(key, value);
442         }
443         return curValue;
444     }
445 
446 
447     IniSection putIfAbsent(string key, IniSection value) {
448         IniSection v = IniSection.init;
449 
450         if (!containsKey(key))
451             v = put(key, value);
452 
453         return v;
454     }
455 
456     bool remove(string key, IniSection value) {
457         IniSection curValue = get(key);
458         if (curValue != value || !containsKey(key))
459             return false;
460         remove(key);
461         return true;
462     }
463 
464     IniSection opIndex(string key) {
465         return get(key);
466     }
467 
468     int opApply(scope int delegate(ref string, ref IniSection) dg) {
469         int result = 0;
470 
471         foreach(string key, IniSection value; sections) {
472             result = dg(key, value);
473         }
474 
475         return result;
476     }
477 
478     int opApply(scope int delegate(MapEntry!(string, IniSection) entry) dg) {
479         int result = 0;
480 
481         foreach(MapEntry!(string, IniSection) entry; sections) {
482             result = dg(entry);
483         }
484 
485         return result;
486     }
487 
488     InputRange!string byKey() {
489         return sections.byKey();
490     }
491 
492     InputRange!IniSection byValue() {
493         return sections.byValue();
494     }
495 
496     bool opEquals(IObject o) {
497         return opEquals(cast(Object) o);
498     }            
499 
500     override bool opEquals(Object obj) {
501         Ini ini = cast(Ini) obj;
502         if (ini !is null) {
503             return this.sections.opEquals(ini.sections);
504         }
505         return false;
506     }
507 
508     override size_t toHash() @trusted nothrow {
509         return this.sections.toHash();
510     }
511 
512     override string toString() {
513         if (this.sections is null || this.sections.isEmpty()) {
514             return "<empty INI>";
515         } else {
516             StringBuilder sb = new StringBuilder("sections=");
517             int i = 0;
518             foreach (IniSection section ; this.sections.values()) {
519                 if (i > 0) {
520                     sb.append(",");
521                 }
522                 sb.append(section.toString());
523                 i++;
524             }
525             return sb.toString();
526         }
527     }
528 
529     int size() {
530         return this.sections.size();
531     }
532 
533     bool containsKey(string key) {
534         return this.sections.containsKey(key);
535     }
536 
537     bool containsValue(IniSection value) {
538         return this.sections.containsValue(value);
539     }
540 
541     IniSection get(string key) {
542         return this.sections.get(key);
543     }
544 
545     IniSection put(string key, IniSection value) {
546         return this.sections.put(key, value);
547     }
548 
549     IniSection remove(string key) {
550         return this.sections.remove(key);
551     }
552 
553     void putAll(Map!(string, IniSection) m) {
554         this.sections.putAll(m);
555     }
556 
557     void clear() {
558         this.sections.clear();
559     }
560 
561     // Set<string> keySet() {
562     //     return Collections.unmodifiableSet(this.sections.keySet());
563     // }
564 
565     IniSection[] values() {
566         return this.sections.values();
567     }
568 
569     // Set<Entry!(string, IniSection)> entrySet() {
570     //     return Collections.unmodifiableSet(this.sections.entrySet());
571     // }
572     
573     mixin CloneMemberTemplate!(typeof(this));
574 
575 }
576 
577 
578 
579 /**
580  * An {@code IniSection} is string-key-to-string-value Map, identifiable by a
581  * {@link #getName() name} unique within an {@link Ini} instance.
582  */
583 class IniSection : Map!(string, string) {
584 
585     enum char ESCAPE_TOKEN = '\\';
586 
587     private string name;
588     private Map!(string, string) props;
589 
590     private this(string name) {
591         trace("section: ", name);
592         if (name.empty) {
593             throw new NullPointerException("name");
594         }
595         this.name = name;
596         this.props = new LinkedHashMap!(string, string)();
597     }
598 
599     private this(string name, string sectionContent) {
600         trace("section: ", name);
601         if (name.empty) {
602             throw new NullPointerException("name");
603         }
604         this.name = name;
605         Map!(string, string) props;
606         if (!sectionContent.empty() ) {
607             props = toMapProps(sectionContent);
608         } else {
609             props = new LinkedHashMap!(string, string)();
610         }
611         if ( props !is null ) {
612             this.props = props;
613         } else {
614             this.props = new LinkedHashMap!(string, string)();
615         }
616     }
617 
618     private this(IniSection defaults) {
619         this(defaults.getName());
620         putAll(defaults.props);
621     }
622 
623     //Protected to access in a test case - NOT considered part of Shiro's API
624 
625     static bool isContinued(string line) {
626         if (line.empty()) {
627             return false;
628         }
629         int length = cast(int)line.length;
630         //find the number of backslashes at the end of the line.  If an even number, the
631         //backslashes are considered escaped.  If an odd number, the line is considered continued on the next line
632         int backslashCount = 0;
633         for (int i = length - 1; i > 0; i--) {
634             if (line[i] == ESCAPE_TOKEN) {
635                 backslashCount++;
636             } else {
637                 break;
638             }
639         }
640         return backslashCount % 2 != 0;
641     }
642 
643     private static bool isKeyValueSeparatorChar(char c) {
644         return isWhite(c) || c == ':' || c == '=';
645     }
646 
647     private static bool isCharEscaped(string s, int index) {
648         return index > 0 && s[index - 1] == ESCAPE_TOKEN;
649     }
650 
651     //Protected to access in a test case - NOT considered part of Shiro's API
652     static string[] splitKeyValue(string keyValueLine) {
653         string line = strip(keyValueLine);
654         if (line.empty()) {
655             return null;
656         }
657         StringBuilder keyBuffer = new StringBuilder();
658         StringBuilder valueBuffer = new StringBuilder();
659 
660         bool buildingKey = true; //we'll build the value next:
661 
662         for (int i = 0; i < cast(int)line.length; i++) {
663             char c = line[i];
664 
665             if (buildingKey) {
666                 if (isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
667                     buildingKey = false;//now start building the value
668                 } else {
669                     keyBuffer.append(c);
670                 }
671             } else {
672                 if (valueBuffer.length() == 0 && isKeyValueSeparatorChar(c) && !isCharEscaped(line, i)) {
673                     //swallow the separator chars before we start building the value
674                 } else {
675                     valueBuffer.append(c);
676                 }
677             }
678         }
679 
680         string key = strip(keyBuffer.toString());
681         string value = strip(valueBuffer.toString());
682 
683         if (key.empty() || value.empty()) {
684             version(HUNT_DEBUG) warningf("key/value is empty: %s = %s, line: %s", key, value, line);
685             string msg = "Line argument must contain a key and a value.  Only one string token was found.";
686             throw new IllegalArgumentException(msg);
687         }
688 
689         version(HUNT_DEBUG_CONFIG) tracef("Discovered key/value pair: %s = %s", key, value);
690 
691         return [key, value];
692     }
693 
694     private static Map!(string, string) toMapProps(string content) {        
695         string line;
696         Map!(string, string) props = new LinkedHashMap!(string, string)();
697         StringBuilder lineBuffer = new StringBuilder();
698         auto scanner = content.lineSplitter();
699 
700         while (!scanner.empty()) {
701             line = strip(scanner.front);
702             scanner.popFront();
703             if (isContinued(line)) {
704                 //strip off the last continuation backslash:
705                 line = line[0 .. $ - 1];
706                 lineBuffer.append(line);
707                 continue;
708             } else {
709                 lineBuffer.append(line);
710             }
711             line = lineBuffer.toString();
712             lineBuffer = new StringBuilder();
713             string[] kvPair = splitKeyValue(line);
714             if(kvPair !is null)
715                 props.put(kvPair[0], kvPair[1]);
716         }
717 
718         return props;
719     }
720 
721     string getName() {
722         return this.name;
723     }
724 
725     void clear() {
726         this.props.clear();
727     }
728 
729     bool containsKey(string key) {
730         return this.props.containsKey(key);
731     }
732 
733     bool containsValue(string value) {
734         return this.props.containsValue(value);
735     }
736 
737     // Set<Entry!(string, string)> entrySet() {
738     //     return this.props.entrySet();
739     // }
740 
741     string get(string key) {
742         return this.props.get(key);
743     }
744 
745     bool isEmpty() {
746         return this.props.isEmpty();
747     }
748 
749     // Set<string> keySet() {
750     //     return this.props.keySet();
751     // }
752 
753     string put(string key, string value) {
754         return this.props.put(key, value);
755     }
756 
757     void putAll(Map!(string, string) m) {
758         this.props.putAll(m);
759     }
760 
761     string remove(string key) {
762         return this.props.remove(key);
763     }
764 
765     int size() {
766         return this.props.size();
767     }
768 
769     string[] values() {
770         return this.props.values();
771     }
772 
773     string opIndex(string key) {
774         return get(key);
775     }
776 
777     bool replace(string key, string oldValue, string newValue) {
778         string curValue = get(key);
779         if (curValue != oldValue || !containsKey(key)) {
780             return false;
781         }
782         put(key, newValue);
783         return true;
784     }
785 
786     string replace(string key, string value) {
787         string curValue = string.init;
788         if (containsKey(key)) {
789             curValue = put(key, value);
790         }
791         return curValue;
792     }
793 
794 
795     string putIfAbsent(string key, string value) {
796         string v = string.init;
797 
798         if (!containsKey(key))
799             v = put(key, value);
800 
801         return v;
802     }
803 
804     bool remove(string key, string value) {
805         string curValue = get(key);
806         if (curValue != value || !containsKey(key))
807             return false;
808         remove(key);
809         return true;
810     }
811     
812 
813     int opApply(scope int delegate(ref string, ref string) dg) {
814         int result = 0;
815         foreach(string key, string value; this.props) {
816             result = dg(key, value);
817         }
818         return result;
819     }
820 
821     int opApply(scope int delegate(MapEntry!(string, string) entry) dg) {
822         int result = 0;
823         foreach(MapEntry!(string, string) entry; this.props) {
824             result = dg(entry);
825         }
826         return result;
827     }
828 
829     InputRange!string byKey() {
830         return this.props.byKey();
831     }
832 
833     InputRange!string byValue() {
834         return this.props.byValue();
835     }
836 
837     bool opEquals(IObject o) {
838         return opEquals(cast(Object) o);
839     }
840 
841     override string toString() {
842         string name = getName();
843         if (Ini.DEFAULT_SECTION_NAME == name) {
844             return "<default>";
845         }
846         return name;
847     }
848 
849     override
850     bool opEquals(Object obj) {
851         IniSection other = cast(IniSection) obj;
852         if (other !is null) {
853             return getName() == other.getName() && this.props == other.props;
854         }
855         return false;
856     }
857 
858     override size_t toHash() @trusted nothrow {
859         return hashOf(this.name) * 31 + this.props.toHash();
860     }
861 
862     mixin CloneMemberTemplate!(typeof(this));
863 }