{ -------------------------------------------------------------------------- }
{ BigIni.PAS                                                   eh 2002-04-14 }
{ Version 4.11                                                               }
{    Delphi 3/4/5 version                                                    }
{    for a Delphi 1/2 version, please see homepage URL below                 }
{ Unit to read/write *.ini files even greater than 64 kB                     }
{ (till today, the KERNEL.DLL and KERNEL32.DLL do it NOT).                   }

{ (c) Edy Hinzen 1996-2002 - Freeware                                        }
{ Mailto:Edy@Hinzen.de                 (thanks for the resonance yet!)       }
{ http://www.Hinzen.de                 (where you find the latest version)   }

{ -------------------------------------------------------------------------- }
{ The TBigIniFile object is designed to work like TIniFile from the Borland  }
{ unit called IniFiles.                                                      }
{ The following procedures/functions were added:                             }
{    procedure FlushFile              write data to disk                     }
{    procedure ReadAll                copy entire contents to TStrings-object}
{    procedure AppendFromFile         appends from other *.ini               }
{    property  SectionNames                                                  }
{    procedure WriteAnsiString        writes AnsiString types                }
{    function  ReadAnsiString         reads AnsiString types                 }

{ -------------------------------------------------------------------------- }
{ The TBiggerIniFile object is a child object with some functions that came  }
{ in handy at my projects:                                                   }
{    property  TextBufferSize                                                }
{    procedure WriteSectionValues(const aSection: string;                    }
{                                 const aStrings: TStrings);                 }
{              analog to ReadSectionValues, replace/write all lines from     }
{              aStrings into specified section                               }
{    procedure ReadNumberedList(const Section: string;                       }
{                              aStrings: TStrings;                           }
{                              Deflt: String);                               }
{    procedure WriteNumberedList(const Section: string;                      }
{                                aStrings: TStrings);                        }
{    function  ReadColor(const aSection, aKey: string;                       }
{                        aDefault: TColor): TColor;                          }
{    procedure WriteColor(const aSection, aKey: string;                      }
{                         aValue: TColor); virtual;                          }
{    function  ReadFont(const aSection, aKey: string;                        }
{                       aFont: TFont): TFont;                                }
{    procedure WriteFont(const aSection, aKey: string;                       }
{                          aFont: TFont);                                    }
{    function  ReadBinaryData(const aSection, aKey: String;                  }
{                             var Buffer; BufSize: Integer): Integer;        }
{    procedure WriteBinaryData(const aSection, aKey: String;                 }
{                              var Buffer; BufSize: Integer);                }
{    procedure RenameSection(const OldSection, NewSection : String);         }
{    procedure RenameKey(const aSection, OldKey, NewKey : String);           }

{ -------------------------------------------------------------------------- }
{ The TAppIniFile object is a child again.                                   }
{ It's constructor create has no parameters. The filename is the             }
{ application's exename with with extension '.ini' (instead of '.exe').      }
{    constructor Create;                                                     }
{ -------------------------------------------------------------------------- }
{ The TLibIniFile object very similar to TAppIniFile.                        }
{ But if the module is a library (e.g. DLL) the library name is used.        }
{    constructor Create;                                                     }
{ -------------------------------------------------------------------------- }

{ ========================================================================== }
{   This program is distributed in the hope that it will be useful,          }
{   but WITHOUT ANY WARRANTY; without even the implied warranty of           }
{   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                     }
{ ========================================================================== }

{ Programmer's note:                                                         }
{ Okay, this is NOT the fastest code of the world... (the kernel-functions   }
{ xxxxPrivateProfileString aren't, either!). I wrote it as a subproject of   }
{ my EditCdPlayer.EXE which never seems to become finished ...               }
{ Meanwhile, I hope that Microsoft will write new KERNEL routines.           }

{ Version history                                                            }
{ 1.10 faster read by replaceing TStringlist with simple ReadLn instructions }
{      improved FindItemIndex by storing last results                        }
{ 1.20 Ignore duplicate sections                                             }
{      improved (for this use) TStringList child TSectionList                }
{ 1.21 fixed 1.20 bug in case of empty file                                  }
{ 1.22 added ReadNumberedList and WriteNumberedList                          }
{ 1.23 Delphi 1.0 backward compatibility e.g. local class TStringList        }
{ 1.24 added AppendFromFile                                                  }
{ 2.00 Changed compare-routines of aSection Parameters to AnsiCompareText    }
{      to handle case insensitive search in languages with special chars;    }
{      some efforts to increase speed                                        }
{      * new web and e-mail address *                                        }
{ 2.01 implemented modifications/suggestions from Gyula Mészáros,            }
{      Budapest, Hungary - 100263.1465@compuserve.com                        }
{procedure TIniFile.ReadSections(aStrings: TStrings);                        }
{    - The extra 16K file buffer is removeable                               }
{      see property TextBufferSize                                           }
{    - comment lines (beginning with ';') can be ignored                     }
{      set property FlagDropCommentLines to True                             }
{    - invalid lines (which do not contain an '=' sign) can be ignored       }
{      set property FlagFilterOutInvalid to True                             }
{    - white spaces around the '='-sign can be dropped                       }
{      set property FlagDropWhiteSpace to True                               }
{    - surrounding single or double apostrophes from keys can be dropped     }
{      set property FlagDropApostrophes to True                              }
{ 2.01 (continued)                                                           }
{      property SectionNames is now part of TBigIni (instead of TBiggerIni   }
{      added procedure ReadSections (seems new in Delphi 3)                  }
{ 2.02 fixed WriteNumberedList bug                                           }
{      added DeleteKey                                                       }
{      changed Pos() calls to AnsiPos()                                      }
{ 2.03 minor corrections                                                     }
{ 2.04 new flag FlagTrimRight                                                }
{      set it true to strip off white spaces at end of line                  }
{ 2.05 fixed bug in EraseSection                                             }
{ 2.06 For Win32 apps, TAppIniFile now creates ini-filename in correct mixed }
{      case                                                                  }
{      added HasChanged-check routine in WriteNumberedList                   }
{ 2.07 added note [1] and [2]                                                }
{      used new function ListIdentical instead of property named text within }
{      WriteNumberedList for backward-compatibility                          }
{ 3.01 fixed another bug in EraseSection (variable FPrevSectionIndex)        }
{ 3.02 dropped some $IFDEFS related to prior (Delphi 1/2) compatibility code }
{ 3.03 added ReadColor / WriteColor                                          }
{ 3.04 added notice about incombatibility with TMemIniFile.ReadSectionValues }
{ 3.05 fixed TTextBufferSize vs. IniBufferSize bug                           }
{ 3.06 added TBigIniFile.HasSection                                          }
{ 3.07 fixed ClearSectionList bug (variable FPrevIndex)                      }
{ 3.08 fixed EraseDuplicates memory-bug                                      }
{ 3.09 inproved ReadSection: empty and commented tags are removed            }
{      ReadSectionValues now clears target TStrings (if flag set, see 3.10)  }
{      improved handling of TStrings by using BeginUpdate, EndUpdate         }
{ 3.10 partly revided 3.09 change by adding FFlagClearOnReadSectionValues    }
{ 3.11 added TLibIniFile and ModuleName                                      }
{ 3.20 new Methods (for Delphi 5 IniFiles compatibility):                    }
{      clear,                                                                }
{      UpdateFile (same as FlushFile),                                       }
{      SectionExists (same as HasSection),                                   }
{      ReadDate, WriteDate, ReadDateTime, WriteDateTime,                     }
{      ReadFloat, WriteFloat, ReadTime, WriteTime                            }
{ 3.21 Added some exception-handling                                         }
{ 4.00 Introduced tool-class TCommaSeparatedInfo                             }
{      Added TBigIniFile.ReadFont, TBigIniFile.WriteFont                     }
{ 4.01 Added TBigIniFile.ReadAnsiString,TBigIniFile.WriteAnsiString          }
{      added TBiggerIniFile.RenameSection,TBiggerIniFile.RenameKey           }
{      added TBiggerIniFile.ReadBinaryData,TBiggerIniFile.WriteBinaryData    }
{ 4.02 Added compiler directive "$DEFINE UseShortStrings"                    }
{ 4.03 improved ReadString performance                                       }
{ 4.04 Added TBigIniFile.ValueExists                                         }
{ 4.05 fixed HasChanged-bug in RenameSection                                 }
{ 4.10 $DEFINE UseShortStrings deactivated; now uses wide strings as default }
{ 4.11 ReadNumberedList, WriteNumberedList have new (defaulted) parameters   }
{ -------------------------------------------------------------------------- }

{ -------------------------------------------------------------------------- }
{ Question: how can I set these properties _before_ the file is opened?      }
{ Answer: call create with empty filename, look at this sample:              }
{       myIniFile := TBigIniFile.Create('');                                 }
{       myIniFile.FlagDropCommentLines := True;                              }
{       myIniFile.FileName := ('my.ini');                                    }
{........................................................................... }
{ Question: how can I write comment lines into the file?                     }
{ Answer: like this:                                                         }
{       tempStringList := TStringList.Create;                                }
{       tempStringList.Add('; this is a comment line.');                     }
{       BiggerIniFile.WriteSectionValues('important note',TempStringList);   }
{       BiggerIniFile.FlushFile;                                             }
{       tempStringList.Free;                                                 }
{ -------------------------------------------------------------------------- }
unit BigIni;

// activate the following line, if you want to access lines longer than 255 chars:
{ $ DEFINE UseShortStrings}

{$IFDEF UseShortStrings}
{$H-} {using short strings in old pascal style increases speed}
{$ENDIF}

interface


uses Classes;

const
  IniTextBufferSize  = $7000;
                     {Note [1]: don't use more than $7FFFF - it's an integer}

  cIniCount          = 'Count';                 {count keyword}

type
  TEraseSectionCallback = function(const sectionName : string; const sl1,sl2 : TStringList):Boolean of object;

{
TCommaSeparatedInfo is a Tool-Class to read and write multiple parameters/values
from a single, comma-separated string. These parameters are positional.

Please see descendant TCSIFont for some useful example.
}
  TCommaSeparatedInfo = class
  private
    FValues        : TStringList;
    function       GetValue: String;
    function       GetElement(index: Integer): String;
    function       GetInteger(index: Integer): Integer;
    function       GetBoolean(index: Integer): Boolean;
    procedure      SetValue(const Value: String);
    procedure      SetBoolean(index: Integer; const Value: Boolean);
    procedure      SetElement(index: Integer; const Value: String);
    procedure      SetInteger(index: Integer; const Value: Integer);
  public
    constructor    Create;
    destructor     Destroy; override;
    Property       Value : String read GetValue write SetValue;
    Property       Element[index:Integer]: String read GetElement write SetElement; default;
    Property       AsInteger[index:Integer]: Integer read GetInteger write SetInteger;
    Property       AsBoolean[index:Integer]: Boolean read GetBoolean write SetBoolean;
  end;

{
TSectionList is a Tool-Class for TBigIniFile
It's a descendant of TStringList with "enhanced" IndexOf function (and others)
}
  TSectionList = class(TStringList)
  private
    FPrevIndex          : Integer;
  public
    constructor Create;
    function    EraseDuplicates(callBackProc:TEraseSectionCallback) : Boolean;
    function    GetSectionItems(index: Integer): TStringList;
    function    IndexOf(const S: AnsiString): Integer; override;
    function    IndexOfName(const name: string): Integer; //override;
    property    SectionItems[index: Integer]: TStringList Read GetSectionItems;
  end;

  TBigIniFile = class(TObject)
  private
    FEraseSectionCallback : TEraseSectionCallback;
    FFileName             : string;
    FPrevSectionIndex     : Integer;
    FFlagClearOnReadSectionValues,   {set true if clearing wanted}
    FFlagDropCommentLines,           {set false to keep lines starting with ';'}
    FFlagFilterOutInvalid,           {set false to keep lines without '='      }
    FFlagDropWhiteSpace,             {set false to keep white space around '='}
    FFlagDropApostrophes,            {set false to keep apostrophes around key }
    FFlagTrimRight        : Boolean; {set false to keep white space at end of line}

    FSectionList          : TSectionList;


    function    FindItemIndex(const aSection, aKey :string; CreateNew:Boolean;
                              var FoundStringList:TStringList):Integer;
    procedure   SetFileName(const aName : string);
    procedure   ClearSectionList;
  protected
    { teo -moved here from private section just to keep compiler happy }
    FHasChanged           : Boolean;
    FTextBufferSize       : Integer;
  public
    constructor Create(const FileName: string);
    destructor  Destroy; override;

    procedure   AppendFromFile(const aName : string); virtual;
    procedure   Clear; virtual;
    procedure   DeleteKey(const aSection, aKey: string); virtual;
    procedure   EraseSection(const aSection: string); virtual;
    procedure   FlushFile; virtual;
    function    HasSection(const aSection: String): Boolean; virtual;
    function    ReadAnsiString(const aSection, aKey, aDefault: string): AnsiString; virtual;
    procedure   ReadAll(aStrings:TStrings); virtual;
    function    ReadBool(const aSection, aKey: string; aDefault: Boolean): Boolean; virtual;
    function    ReadDate(const aSection, aKey: string; aDefault: TDateTime): TDateTime; virtual;
    function    ReadDateTime(const aSection, aKey: string; aDefault: TDateTime): TDateTime; virtual;
    function    ReadFloat(const aSection, aKey: string; aDefault: Double): Double; virtual;
    function    ReadInteger(const aSection, aKey: string; aDefault: Longint): Longint; virtual;
    procedure   ReadSection(const aSection: string; aStrings: TStrings); virtual;
    procedure   ReadSections(aStrings: TStrings); virtual;
    procedure   ReadSectionValues(const aSection: string; aStrings: TStrings); virtual;
    function    ReadString(const aSection, aKey, aDefault: string): string; virtual;
    function    ReadTime(const aSection, aKey: string; aDefault: TDateTime): TDateTime; virtual;
    function    SectionExists(const aSection: String): Boolean; virtual;
    procedure   UpdateFile; virtual;
    function    ValueExists(const aSection, aValue: string): Boolean; virtual;
    procedure   WriteAnsiString(const aSection, aKey, aValue: AnsiString); virtual;
    procedure   WriteBool(const aSection, aKey: string; aValue: Boolean); virtual;
    procedure   WriteDate(const aSection, aKey: string; aValue: TDateTime); virtual;
    procedure   WriteDateTime(const aSection, aKey: string; aValue: TDateTime); virtual;
    procedure   WriteFloat(const aSection, aKey: string; aValue: Double); virtual;
    procedure   WriteInteger(const aSection, aKey: string; aValue: Longint); virtual;
    procedure   WriteString(const aSection, aKey, aValue: string); virtual;
    procedure   WriteTime(const aSection, aKey: string; aValue: TDateTime); virtual;

    property    EraseSectionCallback: TEraseSectionCallback Read FEraseSectionCallback Write FEraseSectionCallback;
    property    FlagClearOnReadSectionValues : Boolean Read FFlagClearOnReadSectionValues Write FFlagClearOnReadSectionValues;
    property    FlagDropApostrophes  : Boolean Read FFlagDropApostrophes Write FFlagDropApostrophes;
    property    FlagDropCommentLines : Boolean Read FFlagDropCommentLines Write FFlagDropCommentLines;
    property    FlagDropWhiteSpace   : Boolean Read FFlagDropWhiteSpace Write FFlagDropWhiteSpace;
    property    FlagFilterOutInvalid : Boolean Read FFlagFilterOutInvalid Write FFlagFilterOutInvalid;
    property    FlagTrimRight        : Boolean Read FFlagTrimRight Write FFlagTrimRight;
    property    FileName: string Read FFileName Write SetFileName;
    property    SectionNames :TSectionList Read FSectionList;
  end;

  TBiggerIniFile = class(TBigIniFile)
  public
    function    ReadBinaryData(const aSection, aKey: String; var Buffer; BufSize: Integer): Integer; virtual;
    procedure   ReadNumberedList(const Section: string;
                                 aStrings: TStrings;
                                 Deflt: string;
                                 aPrefix: String = '';
                                 IndexStart: Integer = 1); virtual;
    procedure   RenameKey(const aSection, OldKey, NewKey: String); virtual;
    procedure   RenameSection(const OldSection, NewSection : String); virtual;
    procedure   WriteBinaryData(const aSection, aKey: String; var Buffer; BufSize: Integer); virtual;
    procedure   WriteNumberedList(const Section: string;
                                  aStrings: TStrings;
                                  aPrefix: String = '';
                                  IndexStart: Integer = 1); virtual;
    procedure   WriteSectionValues(const aSection: string; const aStrings: TStrings); virtual;

    property    HasChanged : Boolean Read FHasChanged Write FHasChanged;
    property    TextBufferSize : Integer Read FTextBufferSize Write FTextBufferSize;
  end;

  TAppIniFile = class(TBiggerIniFile)
    constructor Create;
  end;

  TLibIniFile = class(TBiggerIniFile)
    constructor Create;
  end;

  function ModuleName(getLibraryName:Boolean) : String;

{ -------------------------------------------------------------------------- }
implementation
uses Windows, SysUtils;
{ -------------------------------------------------------------------------- }

{........................................................................... }
{ classless functions/procedures                                             }
{........................................................................... }

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ function ModuleName
  purpose : get the full path of the current module
  - if flag getLibraryName is set (and the module is a library) then the library
    name is returned. Otherwise the applications name is returned.
  - the result is in proper mixed case (the similar function Application.ExeName
    returns all in uppercase chars)
 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function ModuleName(getLibraryName:Boolean) : String;
var
  Buffer        : array[0..260] of Char;
  aHandle       : THandle;
  thePath       : String;
  theSearchRec  : TSearchRec;
begin
  if getLibraryName then aHandle := HInstance
                    else aHandle := 0;
  SetString(Result, Buffer, GetModuleFileName(aHandle, Buffer, SizeOf(Buffer)));
  { GetModuleFileName returns a result in uppercase letters only }
  { The following FindFirst construct returns the mixed case name }
  thePath      := ExtractFilePath(Result);
  if FindFirst(Result,faAnyFile,theSearchRec) = 0 then
  begin
    Result := thePath+theSearchRec.name;
  end;
  FindClose(theSearchRec);
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function max(a,b:Integer):Integer;
begin
  if a > b then Result := a
           else Result := b;
end;

//------------------------------------------------------------------------------
// check if two StringLists contain identical strings
//------------------------------------------------------------------------------
function ListIdentical(l1,l2:TStringList):Boolean;
var
  ix            : Integer;
begin
  Result := False;
  if l1.count = l2.count then
  begin
    for ix := 0 to l1.count-1 do
    begin
      if (l1[ix] <> l2[ix]) then Exit;
    end;
    Result := True;
  end;
end;

{........................................................................... }
{ class TCommaSeparatedInfo                                                  }
{........................................................................... }

constructor TCommaSeparatedInfo.Create;
begin
  FValues := TStringList.Create;
end;

destructor TCommaSeparatedInfo.Destroy;
begin
  FValues.Free;
  inherited;
end;

function TCommaSeparatedInfo.GetBoolean(index: Integer): Boolean;
begin
  // '1' stands for 'true', any other value for 'false'
  Result := (Element[index] = '1');
end;

function TCommaSeparatedInfo.GetElement(index: Integer): String;
begin
  result := FValues[index];
end;

function TCommaSeparatedInfo.GetInteger(index: Integer): Integer;
begin
  Result := StrToIntDef(Element[index],-1);
end;

function TCommaSeparatedInfo.GetValue: String;
begin
  result := FValues.CommaText;
end;

procedure TCommaSeparatedInfo.SetBoolean(index: Integer;
  const Value: Boolean);
const
  BoolText: array[Boolean] of string[1] = ('', '1');
begin
  SetElement(index, BoolText[Value]);
end;

procedure TCommaSeparatedInfo.SetElement(index: Integer;
  const Value: String);
begin
  while (FValues.Count -1) < Index do FValues.Add('');
  FValues[index] := Value;
end;

procedure TCommaSeparatedInfo.SetInteger(index: Integer;
  const Value: Integer);
begin
  SetElement(index, IntToStr(Value));
end;

procedure TCommaSeparatedInfo.SetValue(const Value: String);
begin
  FValues.CommaText := Value;
end;

{........................................................................... }
{ class TSectionList                                                         }
{........................................................................... }

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ create new instance                                                        }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
constructor TSectionList.Create;
begin
  inherited Create;
  FPrevIndex    := 0;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ access to property SectionItems                                            }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TSectionList.GetSectionItems(index: Integer): TStringList;
begin
  Result := TStringList(Objects[index]);
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ erase duplicate entries                                                    }
{ results TRUE if changes were made                                          }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TSectionList.EraseDuplicates(callBackProc:TEraseSectionCallback) : Boolean;
var
  slDuplicateTracking   : TStringList;
  idxToDelete,
  ixLow,
  ixHigh,
  ix                    : Integer;

  { swap two integer variables }
  procedure SwapInt(var a,b:Integer);
  var
    c : Integer;
  begin
    c := a;
    a := b;
    b := c;
  end;
begin
  Result := False; { no changes made yet }

  if count > 1 then
  begin
    slDuplicateTracking := TStringList.Create;
    slDuplicateTracking.Assign(Self);
    { store current position in the objects field: }
    for ix := 0 to slDuplicateTracking.count-1 do slDuplicateTracking.Objects[ix] := Pointer(ix);
    { sort the list to find out duplicates }
    slDuplicateTracking.Sort;
    ixLow := 0;
    for ix := 1 to slDuplicateTracking.count-1 do
    begin
      if (AnsiCompareText(slDuplicateTracking.STRINGS[ixLow],
                          slDuplicateTracking.STRINGS[ix]) <> 0) then
      begin
        ixLow := ix;
      end else
      begin
        ixHigh := ix;
        { find the previous entry (with lower integer number) }
        if Integer(slDuplicateTracking.Objects[ixLow]) >
           Integer(slDuplicateTracking.Objects[ixHigh]) then SwapInt(ixHigh,ixLow);

        if Assigned(callBackProc) then
        begin
          { ask callback/user wether to delete the higher (=true)
            or the lower one (=false)}
          if NOT callBackProc(slDuplicateTracking.STRINGS[ix],
                              SectionItems[Integer(slDuplicateTracking.Objects[ixLow])],
                              SectionItems[Integer(slDuplicateTracking.Objects[ixHigh])])
             then SwapInt(ixHigh,ixLow);
        end;
        idxToDelete := Integer(slDuplicateTracking.Objects[ixHigh]);

        { free associated object and mark it as unassigned }
        SectionItems[idxToDelete].Free;
        Objects[idxToDelete] := nil;
        Result := True; { list had been changed }
      end {if};
    end {for};

    ix := 0;
    while ix < count do
    begin
      if Objects[ix] = nil then Delete(ix)
                           else Inc(ix);
    end;
    slDuplicateTracking.Free;
  end {if};

end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ search string                                                              }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TSectionList.IndexOf(const S: AnsiString): Integer;
var
  ix,
  LastIX        : Integer;
  { This routine doesn't search from the first item each time,
    but from the last successful item. It is likely that the
    next item is to be found downward. }
begin
  Result := -1;
  if count = 0 then Exit;

  LastIX := FPrevIndex;
  { Search from last successful point to the end: }
  for ix := LastIX to count-1 do
  begin
    if (AnsiCompareText(Strings[ix], S) = 0) then begin
      Result     := ix;
      FPrevIndex := ix;
      Exit;
    end;
  end;
  { Not found yet? Search from beginning to last successful point: }
  for ix := 0 to LastIX-1 do
  begin
    if (AnsiCompareText(Strings[ix], S) = 0) then begin
      Result     := ix;
      FPrevIndex := ix;
      Exit;
    end;
  end;
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }

function TSectionList.IndexOfName(const name: string): Integer;
var
  P: Integer;
  s1,
  s2 : AnsiString;
begin
  s2 := name;
  for Result := 0 to Count - 1 do
  begin
    s1 := Strings[Result];
    P := AnsiPos('=', s1);
    SetLength(s1,P-1);
    if (P <> 0) AND (
    CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE,
      PChar(s1), -1,
      PChar(s2), -1)
      = 2) then Exit;
  end;
  Result := -1;
end;

{........................................................................... }
{ class TBigIniFile                                                          }
{........................................................................... }

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ create new instance                                                        }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
constructor TBigIniFile.Create(const FileName: string);
begin
  FSectionList    := TSectionList.Create;
  FTextBufferSize := IniTextBufferSize;   { you may set to zero to switch off }
  FFlagDropCommentLines := False;         { change this aDefaults if needed }
  FFlagFilterOutInvalid := False;
  FFlagDropWhiteSpace   := False;
  FFlagDropApostrophes  := False;
  FFlagTrimRight        := False;
  FFlagClearOnReadSectionValues := False;
  FFileName             := '';
  FPrevSectionIndex     := 0;
  FEraseSectionCallback := nil;
  SetFileName(FileName);
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ destructor                                                                 }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
destructor TBigIniFile.Destroy;
begin
  FlushFile;
  ClearSectionList;
  FSectionList.Free;
  inherited Destroy;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ clean up                                                                   }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.ClearSectionList;
var
  ixSections      : Integer;
begin
  with FSectionList do
  begin
    for ixSections := 0 to count -1 do
    begin
      SectionItems[ixSections].Free;
    end;
    Clear;
    FPrevIndex := 0;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ Erases all data from the INI file                                          }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.Clear;
begin
  ClearSectionList;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ Append from File                                                           }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.AppendFromFile(const aName : string);
var
  CurrStringList  : TStringList;
  CurrSectionName : string;
  lpTextBuffer    : Pointer;
  Source          : TextFile;
  OneLine         : string;
  LL              : Integer;
  LastPos,
  EqPos           : Integer;
  nospace         : Boolean;
begin
  CurrStringList  := nil;
  lpTextBuffer    := nil; {only to avoid compiler warnings}
  FPrevSectionIndex := 0;
  if FileExists(aName) then
  begin
    Assign      (Source,aName);
    if FTextBufferSize > 0 then
    begin
      GetMem(lpTextBuffer,FTextBufferSize);
      SetTextBuf(Source,lpTextBuffer^,FTextBufferSize);
    end;
    Reset       (Source);
    while NOT Eof(Source) do
    begin
      ReadLn(Source,OneLine);
      if OneLine = #$1A {EOF} then OneLine := '';
      { drop lines with leading ';' : }
      if FFlagDropCommentLines then if OneLine <> '' then if (OneLine[1] = ';') then OneLine := '';
      { drop lines without '=' }
      if OneLine <> '' then begin
        LL := Length(OneLine);
        if (OneLine[1] = '[') AND (OneLine[LL] = ']') then
        begin
          CurrSectionName := Copy(OneLine,2,LL-2);
          CurrStringList  := TStringList.Create;
          FSectionList.AddObject(CurrSectionName,CurrStringList);
        end
        else begin
          if FFlagDropWhiteSpace then
          begin
            nospace := False;
            repeat
              { delete white space left to equal sign }
              EqPos := AnsiPos('=', OneLine);
              if EqPos > 1 then begin
                if OneLine[EqPos - 1] IN [' ', #9] then
                  Delete(OneLine, EqPos - 1, 1)
                else
                  nospace := True;
              end
              else
                nospace := True;
            until nospace;
            nospace := False;
            EqPos := AnsiPos('=', OneLine);
            if EqPos > 1 then begin
              repeat
                { delete white space right to equal sign }
                if EqPos < Length(OneLine) then begin
                  if OneLine[EqPos + 1] IN [' ', #9] then
                    Delete(OneLine, EqPos + 1, 1)
                  else
                    nospace := True;
                end
                else
                  nospace := True;
              until nospace;
            end;
          end; {FFlagDropWhiteSpace}
          if FFlagDropApostrophes then
          begin
            EqPos := AnsiPos('=', OneLine);
            if EqPos > 1 then begin
              LL := Length(OneLine);
              { filter out the apostrophes }
              if EqPos < LL - 1 then begin
                if (OneLine[EqPos + 1] = OneLine[LL]) AND (OneLine[LL] IN ['"', #39]) then
                begin
                  Delete(OneLine, LL, 1);
                  Delete(OneLine, EqPos + 1, 1);
                end;
              end;
            end;
          end; {FFlagDropApostrophes}
          if FFlagTrimRight then
          begin
            LastPos := Length(OneLine);
            while ((LastPos > 0) AND (OneLine[LastPos] < #33)) do Dec(LastPos);
            OneLine := Copy(OneLine,1,LastPos);
          end; {FFlagTrimRight}
          if (NOT FFlagFilterOutInvalid) OR (AnsiPos('=', OneLine) > 0) then
          begin
            if Assigned(CurrStringList) then CurrStringList.Add(OneLine);
          end;
        end;
      end;
    end;

    if FSectionList.EraseDuplicates(FEraseSectionCallback) then FHasChanged := True;

    Close(Source);
    if FTextBufferSize > 0 then
    begin
      FreeMem(lpTextBuffer,FTextBufferSize);
    end;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ Set or change FileName                                                     }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.SetFileName(const aName : string);
begin
  FlushFile;
  ClearSectionList;
  FFileName       := aName;
  if aName <> '' then AppendFromFile(aName);
  FHasChanged     := False;
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ find item in specified section                                             }
{ depending on CreateNew-flag, the section is created, if not existing       }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.FindItemIndex(const aSection, aKey :string; CreateNew:Boolean;
          var FoundStringList:TStringList):Integer;
var
  SectionIndex    : Integer;
  LastIX          : Integer;
begin
  SectionIndex := -1;

  if FSectionList.count > 0 then
  begin
    LastIX := FPrevSectionIndex -1;
    if LastIX < 0 then LastIX := FSectionList.count -1;
    while (AnsiCompareText(aSection,FSectionList[FPrevSectionIndex]) <> 0)
      AND (FPrevSectionIndex <> LastIX) do
    begin
      Inc(FPrevSectionIndex);
      if FPrevSectionIndex = FSectionList.count then FPrevSectionIndex := 0;
    end;
    if AnsiCompareText(aSection,FSectionList[FPrevSectionIndex]) = 0 then
    begin
      SectionIndex := FPrevSectionIndex;
    end;
  end;

  if SectionIndex = -1 then
  begin
    if CreateNew then begin
      FoundStringList   := TStringList.Create;
      FPrevSectionIndex := FSectionList.AddObject(aSection,FoundStringList);
    end
    else begin
      FoundStringList := nil;
    end;
    Result := -1;
  end
  else begin
    FoundStringList := FSectionList.SectionItems[SectionIndex];
    Result := FoundStringList.IndexOfName(aKey);
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ the basic function: return single string                                   }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ReadString(const aSection, aKey, aDefault: string): string;
var
  ItemIndex       : Integer;
  CurrStringList  : TStringList;
begin
  ItemIndex := FindItemIndex(aSection,aKey,False,CurrStringList);
  if ItemIndex = -1 then
  begin
    Result := aDefault
  end
  else begin
    Result := Copy(CurrStringList[ItemIndex], Length(aKey) + 2, MaxInt);
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ same as ReadString, but returns AnsiString type                            }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ReadAnsiString(const aSection, aKey, aDefault: string): AnsiString;
var
  ItemIndex       : Integer;
  CurrStringList  : TStringList;
begin
  ItemIndex := FindItemIndex(aSection,aKey,False,CurrStringList);
  if ItemIndex = -1 then
  begin
    Result := aDefault
  end
  else begin
    Result := CurrStringList.Values[aKey];
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ here is the one to write the string                                        }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.WriteString(const aSection, aKey, aValue: string);
var
  ItemIndex       : Integer;
  CurrStringList  : TStringList;
  newLine         : string;
begin
  if aKey = '' then
  begin
    {behaviour of WritePrivateProfileString: if all parameters are null strings,
     the file is flushed to disk. Otherwise, if key name is a null string,
     the entire Section is to be deleted}
    if (aSection = '') AND (aValue = '') then FlushFile
                                         else EraseSection(aSection);
  end
  else begin
    newLine := aKey+'='+aValue;
    ItemIndex := FindItemIndex(aSection,aKey,True,CurrStringList);
    if ItemIndex = -1 then begin
      CurrStringList.Add(newLine);
      FHasChanged   := True;
    end
    else begin
      if (CurrStringList[ItemIndex] <> newLine) then
      begin
        FHasChanged := True;
        CurrStringList[ItemIndex] := newLine;
      end;
    end;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ Same as writestring, but processes AnsiString type                         }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.WriteAnsiString(const aSection, aKey, aValue: AnsiString);
var
  ItemIndex       : Integer;
  CurrStringList  : TStringList;
  newLine         : AnsiString;
begin
  if aKey = '' then
  begin
    {behaviour of WritePrivateProfileString: if all parameters are null strings,
     the file is flushed to disk. Otherwise, if key name is a null string,
     the entire Section is to be deleted}
    if (aSection = '') AND (aValue = '') then FlushFile
                                         else EraseSection(aSection);
  end
  else begin
    newLine := aKey+'='+aValue;
    ItemIndex := FindItemIndex(aSection,aKey,True,CurrStringList);
    if ItemIndex = -1 then begin
      CurrStringList.Add(newLine);
      FHasChanged   := True;
    end
    else begin
      if (CurrStringList[ItemIndex] <> newLine) then
      begin
        FHasChanged := True;
        CurrStringList[ItemIndex] := newLine;
      end;
    end;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read integer value                                                         }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ReadInteger(const aSection, aKey: string;
  aDefault: Longint): Longint;
var
  IStr: string;
begin
  IStr := ReadString(aSection, aKey, '');
  if CompareText(Copy(IStr, 1, 2), '0x') = 0 then
    IStr := '$' + Copy(IStr, 3, 255);
  Result := StrToIntDef(IStr, aDefault);
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ Yes, you guessed right: this procedure writes an integer value             }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.WriteInteger(const aSection, aKey: string; aValue: Longint);
begin
  WriteString(aSection, aKey, IntToStr(aValue));
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read boolean value                                                         }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ReadBool(const aSection, aKey: string;
  aDefault: Boolean): Boolean;
begin
  Result := ReadInteger(aSection, aKey, Ord(aDefault)) <> 0;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ write boolean value                                                        }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.WriteBool(const aSection, aKey: string; aValue: Boolean);
const
  BoolText: array[Boolean] of string[1] = ('0', '1');
begin
  WriteString(aSection, aKey, BoolText[aValue]);
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read date value                                                            }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ReadDate(const aSection, aKey: string; aDefault: TDateTime): TDateTime;
var
  DateStr: string;
begin
  DateStr := ReadString(aSection, aKey, '');
  Result  := aDefault;
  if DateStr <> '' then
  try
    Result := StrToDate(DateStr);
  except
    on EConvertError do
    else raise;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ write date value                                                           }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.WriteDate(const aSection, aKey: string; aValue: TDateTime);
begin
  WriteString(aSection, aKey, DateToStr(aValue));
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read DateTime value                                                        }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ReadDateTime(const aSection, aKey: string; aDefault: TDateTime): TDateTime;
var
  DateStr: string;
begin
  DateStr := ReadString(aSection, aKey, '');
  Result := aDefault;
  if DateStr <> '' then
  try
    Result := StrToDateTime(DateStr);
  except
    on EConvertError do
    else raise;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ write DateTime value                                                       }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.WriteDateTime(const aSection, aKey: string; aValue: TDateTime);
begin
  WriteString(aSection, aKey, DateTimeToStr(aValue));
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read Float value                                                           }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ReadFloat(const aSection, aKey: string; aDefault: Double): Double;
var
  FloatStr: string;
begin
  FloatStr := ReadString(aSection, aKey, '');
  Result := aDefault;
  if FloatStr <> '' then
  try
    Result := StrToFloat(FloatStr);
  except
    on EConvertError do
    else raise;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ write Float value                                                          }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.WriteFloat(const aSection, aKey: string; aValue: Double);
begin
  WriteString(aSection, aKey, FloatToStr(aValue));
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read Time value                                                            }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ReadTime(const aSection, aKey: string; aDefault: TDateTime): TDateTime;
var
  TimeStr: string;
begin
  TimeStr := ReadString(aSection, aKey, '');
  Result := aDefault;
  if TimeStr <> '' then
  try
    Result := StrToTime(TimeStr);
  except
    on EConvertError do
    else raise;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ write Time value                                                           }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.WriteTime(const aSection, aKey: string; aValue: TDateTime);
begin
  WriteString(aSection, aKey, TimeToStr(aValue));
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read entire section (hoho, only the item names)                            }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.ReadSection(const aSection: string; aStrings: TStrings);
var
  SectionIndex    : Integer;
  CurrStringList  : TStringList;
  ix              : Integer;
begin
  SectionIndex := FSectionList.IndexOf(aSection);
  if SectionIndex <> -1 then
  begin
    CurrStringList := FSectionList.SectionItems[SectionIndex];
    for ix := 0 to CurrStringList.count - 1 do
    begin
      if CurrStringList.Names[IX] = '' then continue;
      if FFlagDropCommentLines and (CurrStringList.Names[IX][1] = ';') then continue;
      aStrings.Add(CurrStringList.Names[ix]);
    end;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ copy all section names to TStrings object                                  }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.ReadSections(aStrings: TStrings);
begin
  aStrings.Assign(SectionNames);
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read entire section                                                        }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.ReadSectionValues(const aSection: string; aStrings: TStrings);
var
  SectionIndex    : Integer;
begin
  SectionIndex := FSectionList.IndexOf(aSection);
  if SectionIndex <> -1 then
  begin
    {In prior versions of TIniFile the target-Strings were _not_ cleared
    That's why my procedure didn't either. Meanwhile, Borland changed their
    mind and I added the following line for D5 compatibility.
    Use ReadAppendSectionValues if needed}
    if FFlagClearOnReadSectionValues then aStrings.Clear; // new since 3.09,3.10
    aStrings.AddStrings(FSectionList.SectionItems[SectionIndex]);
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ copy all 'lines' to TStrings-object                                        }
{ Note [2]: under Delphi 1, ReadAll may cause errors when a TMemo.Lines      }
{      array is destination and source is greater than 64 KB                 }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.ReadAll(aStrings:TStrings);
var
  ixSections      : Integer;
  CurrStringList  : TStringList;
begin
  with FSectionList do
  begin
    for ixSections := 0 to count -1 do
    begin
      CurrStringList := SectionItems[ixSections];
      if CurrStringList.count > 0 then
      begin
        aStrings.Add('['+STRINGS[ixSections]+']');
        aStrings.AddStrings(CurrStringList);
        aStrings.Add('');
      end;
    end;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ flush (save) data to disk                                                  }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.FlushFile;
var
  CurrStringList  : TStringList;
  lpTextBuffer    : Pointer;
  Destin          : TextFile;
  ix,
  ixSections      : Integer;
begin
  lpTextBuffer    := nil; {only to avoid compiler warnings}
  if FHasChanged then
  begin
    if FFileName <> '' then
    begin
      Assign     (Destin,FFileName);
      if FTextBufferSize > 0 then
      begin
        GetMem(lpTextBuffer,FTextBufferSize);
        SetTextBuf (Destin,lpTextBuffer^,FTextBufferSize);
      end;
      Rewrite    (Destin);

      with FSectionList do
      begin
        for ixSections := 0 to count -1 do
        begin
          CurrStringList := SectionItems[ixSections];
          if CurrStringList.count > 0 then
          begin
            WriteLn(Destin,'[',STRINGS[ixSections],']');
            for ix := 0 to CurrStringList.count -1 do
            begin
              WriteLn(Destin,CurrStringList[ix]);
            end;
            WriteLn(Destin);
          end;
        end;
      end;

      Close(Destin);
      if FTextBufferSize > 0 then
      begin
        FreeMem(lpTextBuffer,FTextBufferSize);
      end;
    end;
    FHasChanged   := False;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ Flushes buffered INI file data to disk                                     }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.UpdateFile;
begin
  FlushFile;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ erase specified section                                                    }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.EraseSection(const aSection: string);
var
  SectionIndex    : Integer;
begin
  SectionIndex := FSectionList.IndexOf(aSection);
  if SectionIndex <> -1 then
  begin
    FSectionList.SectionItems[SectionIndex].Free;
    FSectionList.Delete(SectionIndex);
    FSectionList.FPrevIndex  := 0;
    FHasChanged := True;
    if FPrevSectionIndex >= FSectionList.count then FPrevSectionIndex := 0;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ remove a single key                                                        }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBigIniFile.DeleteKey(const aSection, aKey: string);
var
  ItemIndex       : Integer;
  CurrStringList  : TStringList;
begin
  ItemIndex := FindItemIndex(aSection,aKey,True,CurrStringList);
  if ItemIndex > -1 then begin
    FHasChanged := True;
    CurrStringList.Delete(ItemIndex);
  end;
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ check for existance of a section                                           }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.HasSection(const aSection: String): Boolean;
begin
  Result := (FSectionList.IndexOf(aSection) > -1)
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ Indicates whether a section exists                                         }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.SectionExists(const aSection: String): Boolean;
begin
  Result := HasSection(aSection);
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ Indicates whether a key exists                                             }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBigIniFile.ValueExists(const aSection, aValue: string): Boolean;
var
  S: TStringList;
begin
  S := TStringList.Create;
  try
    ReadSection(aSection, S);
    Result := S.IndexOf(aValue) > -1;
  finally
    S.Free;
  end;
end;

{........................................................................... }
{ class TBiggerIniFile                                                       }
{........................................................................... }

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ write/replace complete section                                             }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBiggerIniFile.WriteSectionValues(const aSection: string; const aStrings: TStrings);
var
  SectionIndex    : Integer;
  FoundStringList : TStringList;
  ix              : Integer;
begin
  SectionIndex := FSectionList.IndexOf(aSection);
  if SectionIndex = -1 then
  begin
    { create new section }
    FoundStringList := TStringList.Create;
    FSectionList.AddObject(aSection,FoundStringList);
    FoundStringList.AddStrings(aStrings);
    FHasChanged := True;
  end
  else begin
    { compare existing section }
    FoundStringList := FSectionList.SectionItems[SectionIndex];
    if FoundStringList.count <> aStrings.count then
    begin
      { if count differs, replace complete section }
      FoundStringList.Clear;
      FoundStringList.AddStrings(aStrings);
      FHasChanged := True;
    end
    else begin
      { compare line by line }
      for ix := 0 to FoundStringList.count - 1 do
      begin
        if FoundStringList[ix] <> aStrings[ix] then
        begin
          FoundStringList[ix] := aStrings[ix];
          FHasChanged := True;
        end;
      end;
    end;
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ read a numbered list                                                       }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBiggerIniFile.ReadNumberedList(const Section: string;
                                     aStrings: TStrings;
                                     Deflt: string;
                                     aPrefix: String = '';
                                     IndexStart: Integer = 1);
var
  maxEntries  : Integer;
  ix          : Integer;
begin
  maxEntries := ReadInteger(Section,cIniCount,0);
  for ix := 0 to maxEntries -1 do begin
    aStrings.Add(ReadString(Section,aPrefix+IntToStr(ix+IndexStart),Deflt));
  end;
end;
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ write a numbered list (TStrings contents)                                  }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBiggerIniFile.WriteNumberedList(const Section: string;
                                           aStrings: TStrings;
                                           aPrefix: String = '';
                                           IndexStart: Integer = 1);
var
  prevCount,
  ix                    : Integer;
  prevHasChanged        : Boolean;
  oldSectionValues,
  newSectionValues      : TStringList;
begin
  oldSectionValues      := TStringList.Create;
  newSectionValues      := TStringList.Create;

  try
    { store previous entries }
    ReadSectionValues(Section,oldSectionValues);

    prevCount := ReadInteger(Section,cIniCount,0);
    WriteInteger(Section,cIniCount,aStrings.count);
    prevHasChanged := HasChanged;

    { remove all previous lines to get new ones together }
    for ix := 0 to prevCount-1 do begin
      DeleteKey(Section,aPrefix+IntToStr(ix+IndexStart));
    end;
    for ix := 0 to aStrings.count -1 do begin
      WriteString(Section,aPrefix+IntToStr(ix+IndexStart),aStrings[ix]);
    end;

    { check if entries really had changed }
    if NOT prevHasChanged then
    begin
      { read new entries and compare with old }
      ReadSectionValues(Section,newSectionValues);
      HasChanged := NOT ListIdentical(newSectionValues,oldSectionValues);
    end;
  finally
    oldSectionValues.Free;
    newSectionValues.Free;
  end;
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ renames a section                                                          }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBiggerIniFile.RenameSection(const OldSection, NewSection : String);
var
  SectionIndex    : Integer;
begin
  if NewSection <> OldSection then
  begin
    SectionIndex := FSectionList.IndexOf(OldSection);
    if SectionIndex <> -1 then
    begin
      FSectionList[SectionIndex] := NewSection;
    end;
    FHasChanged := True;
  end;
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ renames a key                                                              }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBiggerIniFile.RenameKey(const aSection, OldKey, NewKey : String);
var
  ItemIndex       : Integer;
  CurrStringList  : TStringList;
begin
  if NewKey <> OldKey then
  begin
    ItemIndex := FindItemIndex(aSection,OldKey,False,CurrStringList);
    if ItemIndex <> -1 then
    begin
      WriteString(aSection,NewKey,ReadString(aSection,OldKey,''));
      DeleteKey(aSection, OldKey);
    end;
  end;
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ reads data into a buffer                                                   }
{ result: actually read bytes                                                }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
function TBiggerIniFile.ReadBinaryData(const aSection, aKey: String; var Buffer; BufSize: Integer): Integer;
var
  ix            : Integer;
  bufPtr        : PChar;
  hexDump       : AnsiString;
begin
  hexDump := ReadAnsiString(aSection,aKey,'');
  result  := Length(hexDump) div 2;
  if result > BufSize then result := BufSize;

  bufPtr  := Pointer(Buffer);
  for ix := 0 to result -1 do
  begin
    Byte(bufPtr[ix]) := StrToIntDef('$' + Copy(hexDump,1 + ix*2,2) ,0);
  end;
end;

{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
{ writes data from a buffer                                                  }
{ each represented byte is stored as hexadecimal string                      }
{. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . }
procedure TBiggerIniFile.WriteBinaryData(const aSection, aKey: String; var Buffer; BufSize: Integer);
var
  ix            : Integer;
  bufPtr        : PChar;
  hexDump       : AnsiString;
begin
  hexDump := '';
  bufPtr  := Pointer(Buffer);
  for ix := 0 to BufSize-1 do
  begin
    hexDump := hexDump + IntToHex(Byte(bufPtr[ix]),2);
  end;
  WriteAnsiString(aSection,aKey,hexDump);
end;

{........................................................................... }
{ class TAppIniFile                                                          }
{........................................................................... }
constructor TAppIniFile.Create;
begin
  inherited Create(ChangeFileExt(ModuleName(False),'.ini'));
end;

{........................................................................... }
{ class TLibIniFile                                                          }
{........................................................................... }
constructor TLibIniFile.Create;
begin
  inherited Create(ChangeFileExt(ModuleName(True),'.ini'));
end;

end.

