TRichIniFile is a Delphi 12+ drop-in friendly INI engine that keeps the source file intact while exposing a familiar TIniFile/TMemIniFile surface. It now descends from System.IniFiles.TCustomIniFile, so APIs that expect a TCustomIniFile instance can use it directly. It is designed for tooling and configuration scenarios that need comment preservation, duplicate keys, controlled encodings, and atomic saves without sacrificing performance.
- Preserves the original text layout: comments, blank lines, duplicate sections/keys, and key ordering are kept byte-for-byte unless you mutate them.
- Comment ownership modes (
coAttachToNext,coAttachToPrev,coNone) let you control where runs of comments travel during edits. - Duplicate-aware API: append keys, enumerate all duplicate values, ask for the last occurrence index, and consolidate sections when you need a single view.
- Encoding and newline control: auto-detect BOM/encoding on load, force UTF-8/ANSI/custom encodings on save, and override newline style per spec.
TCustomIniFilecompatibility:UpdateFile,ReadBool,WriteBool, and the standard INI read/write surface work through the RichIni engine.- Atomic
SaveToFile: writes to a sibling temp file and renames into place, mirroring the detected BOM and newline semantics unless overridden. - Compatibility surface mirrors
TMemIniFile(ReadString,WriteString,ReadSection*,EraseSection, etc.), so existing consumers can migrate with minimal changes.
var
Options: TRichIniOptions;
Ini: TRichIniFile;
begin
Options := DefaultRichIniOptions;
Options.CaseSensitivity := csCaseInsensitive;
Ini := TRichIniFile.Create('settings.ini', Options);
try
// work with Ini
finally
Ini.Free;
end;
end;Key option knobs (see MaxLogic.RichIniFile.pas for full detail):
| Option | Purpose |
|---|---|
LoadEncoding / SaveEncoding |
Choose between auto-detect, UTF-8, ANSI, or a caller-supplied TEncoding. |
BomPolicy |
Reuse the source BOM, force one, or suppress it entirely. |
NewlineMode |
Preserve the input newline convention, use the platform default, or force CRLF/LF. |
CaseSensitivity |
Toggle between case-sensitive (Ordinal) and case-insensitive (OrdinalIgnoreCase) lookups. |
AcceptMissingBracket |
Decide how to treat malformed section headers ([Section). |
CommentPrefixes |
Customize which prefixes (;, #, // by default) are treated as full-line comments. |
CommentOwnership |
Route comment runs to their next/previous owner or keep them orphaned. |
KeyValueDelimiter |
Accept alternate delimiters such as : or space. |
BooleanTrueValues |
Case-insensitive tokens treated as True when reading booleans (1, y, yes, on, enabled, true by default). |
BooleanTrueValue / BooleanFalseValue |
Tokens written by WriteBool (1 / 0 by default). |
Use CreateFromStrings when you already have the text in memory and want to avoid filesystem IO:
Ini := TRichIniFile.CreateFromStrings(SourceLines, Options);Use LoadFromText when we already decoded the file content ourselves and still want TRichIniFile to preserve the source newline/BOM/encoding semantics for later SaveToFile calls:
Ini.LoadFromText(SourceText, TEncoding.UTF8, True);Use TRichIniFile anywhere a TCustomIniFile is expected:
var
IniBase: TCustomIniFile;
begin
IniBase := TRichIniFile.Create('settings.ini', Options);
try
IniBase.WriteBool('Main', 'Enabled', True); // writes "1" by default
IniBase.UpdateFile;
finally
IniBase.Free;
end;
end;Value := Ini.ReadString('Database', 'User', 'fallback');
Ini.WriteString('Database', 'Password', 'secret');
Ini.SaveToFile; // optional: path argument overrides the constructor targetCompatibility methods collapse duplicates (last occurrence wins), mirroring RTL semantics. Rich extensions expose the duplicate stream:
AppendKey→ appends a new key occurrence and returns its index.ReadAllKeyValues→ fetches every value for a duplicate key.LastKeyIndex,KeyCount→ track how many duplicates exist and where the last one lives.ReadComment/WriteComment/PurgeComments→ inspect or mutate comment blocks.ConsolidateSection/ConsolidateAll→ squashes duplicate keys while keeping the newest values.ReadMultilineString/WriteMultilineString→ encode/decode multi-line payloads using the spec’s helper format.
Global sections are supported (aSection = '') and never emit headers on save. Section/Key order is preserved by keeping an ordered list alongside lookup dictionaries, giving O(1) lookups and predictable save ordering.
- Loading detects BOM and newline style once, normalizes section/key tokens through
TFastCaseAwareComparer.Ordinal/TFastCaseAwareComparer.OrdinalIgnoreCase, and tracks dirty state. - Writes mutate the in-memory AST and mark affected lines “dirty”; untouched lines are emitted verbatim.
SaveToFileresolves the outgoing encoding/newline according to the option set, writes to a temporary file in the destination folder, then replaces the target atomically.
- Inline (same-line) comments remain unsupported; the parser keeps them as part of the value.
- INI values are single-line by nature; for multi-line text we use the built-in helpers
WriteMultilineString/ReadMultilineString. These methods store line breaks inside the value using a simple escape format: newlines become\nand literal backslashes become\\. - Options are set at construction time in the current public API; create a fresh instance if we need different comparers or comment behavior.
- Unit tests:
tests/unit/MaxLogic.RichIniFiles.Tests.pas(registered intests/MaxLogic.Tests.dpr). Run them via./tests/build-tests.shfollowed by./tests/MaxLogic.Tests.exe. - Benchmarks:
benchmarks/MaxLogic.RichIniBenchmark.dprcomparesTRichIniFile,TMemIniFile, andTIniFileacross load/read/write/save workloads and multiple INI sizes (Tiny/Small/Medium/Large). Build with./tests/build-delphi.sh benchmarks/MaxLogic.RichIniBenchmark.dprojand runbenchmarks/Win32/MaxLogic.RichIniBenchmark.exe(CLI:--iterations=N,--warmup=N).
Observations from our benchmark runs (15 iterations, warmup=3) with INIs in the ~2 KB → ~160 KB range:
- Load:
TRichIniFileis typically ~1.5×–2× slower thanTMemIniFilebecause we build a richer model (preserve ordering, comments, duplicates). - Read:
TRichIniFileis typically ~2×–4× faster thanTMemIniFilefor repeatedReadStringcalls (dictionary lookup dominates). - Write (in-memory):
WriteStringis effectively O(1) and very cheap; absolute timings are tiny and can be noisy. - Save: for small INIs, total save time is often dominated by our atomic-save path (temp file + replace + flush), so it looks almost “flat” across Tiny/Small/Medium sizes and has higher variance. As the file grows, the gap vs
TMemIniFile.UpdateFilenarrows.
Rule of thumb: if our workload is “load once, read many keys”, TRichIniFile typically breaks even after only a handful of reads. If our workload is “write + save in a tight loop”, TMemIniFile will usually stay faster.
Pick TRichIniFile when you need any of:
- Preservation of layout and comments for round-tripping user-edited INI files.
- Duplicate section/key awareness beyond “last wins”.
- Strong control over encoding/newline policy during save.
- Comment ownership that survives edits without manual bookkeeping.
- Atomic saves to avoid partial writes or file corruption on failure.
Stick to TIniFile or TMemIniFile when we only need quick key/value access and do not care about formatting or duplicates (or when we need to save extremely frequently and want the lowest possible save latency).