///*********************************************************************************************************************
///  $Id: DKL_ResFile.pas,v 1.4 2006/08/11 06:16:56 dale Exp $
///---------------------------------------------------------------------------------------------------------------------
///  DKLang Localization Package
///  Copyright 2002-2006 DK Software, http://www.dk-soft.org
///*********************************************************************************************************************
///
/// The contents of this package are subject to the Mozilla Public License
/// Version 1.1 (the "License"); you may not use this file except in compliance
/// with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
///
/// Alternatively, you may redistribute this library, use and/or modify it under the
/// terms of the GNU Lesser General Public License as published by the Free Software
/// Foundation; either version 2.1 of the License, or (at your option) any later
/// version. You may obtain a copy of the LGPL at http://www.gnu.org/copyleft/
///
/// Software distributed under the License is distributed on an "AS IS" basis,
/// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
/// specific language governing rights and limitations under the License.
///
/// The initial developer of the original code is Dmitry Kann, http://www.dk-soft.org/
///
///**********************************************************************************************************************
// Routines and classes to handle .res resource files.
// WARNING: TDKLang_ResFile only handles 32-bit resource files and does not support 16-bit ones!
//
unit DKL_ResFile;

interface
uses Windows, SysUtils, Classes, Contnrs, TntClasses;

type

   //===================================================================================================================
   // Resource entry header
   //===================================================================================================================

  PResResourceEntryHeader = ^TResResourceEntryHeader;
  TResResourceEntryHeader = packed record
    iDataSize:   Integer; // Data size in bytes
    iHeaderSize: Integer; // Header size in bytes
  end;

   //===================================================================================================================
   // Resource entry properties
   //===================================================================================================================

  PResResourceEntryProps = ^TResResourceEntryProps;
  TResResourceEntryProps = packed record
    cDataVersion:     Cardinal;
    wMemoryFlags:     Word;
    wLanguage:        LANGID;
    cVersion:         Cardinal;
    cCharacteristics: Cardinal;
  end;

   //===================================================================================================================
   // .res file handler
   //===================================================================================================================

  TDKLang_ResEntry = class;

  TDKLang_ResFile = class(TObject)
  private
     // Entry list
    FEntries: TObjectList;
     // Loads .res file contents from the stream
    procedure LoadFromStream(Stream: TStream);
     // Saves .res file contents into the stream
    procedure SaveToStream(Stream: TStream);
    function GetEntries(Index: Integer): TDKLang_ResEntry;
    function GetEntryCount: Integer;
  public
    constructor Create;
    destructor Destroy; override;
     // Adds an entry and returns its index
    function  AddEntry(Item: TDKLang_ResEntry): Integer;
     // Deletes the entry
    procedure DeleteEntry(Index: Integer);
     // Removes the entry and returns index it had before the deletion
    function  RemoveEntry(Item: TDKLang_ResEntry): Integer;
     // Clears the entry list
    procedure ClearEntries;
     // Loads .res file contents from the file
    procedure LoadFromFile(const wsFileName: WideString);
     // Saves .res file contents into the file
    procedure SaveToFile(const wsFileName: WideString);
     // Tries to find an entry by its type and name. Returns nil if not found
    function  FindEntry(const wsType, wsName: WideString): TDKLang_ResEntry;
     // Props
     // -- Entry count
    property EntryCount: Integer read GetEntryCount;
     // -- Entries by index
    property Entries[Index: Integer]: TDKLang_ResEntry read GetEntries; default;
  end;

   //===================================================================================================================
   // Single resource entry
   //===================================================================================================================

  TDKLang_ResEntry = class(TObject)
  private
     // Prop storage
    FCharacteristics: Cardinal;
    FDataVersion: Cardinal;
    FLanguage: LANGID;
    FMemoryFlags: Word;
    FName: WideString;
    FRawData: String;
    FResType: WideString;
    FVersion: Cardinal;
  public
     // Stores resource entry into the stream
    procedure SaveToStream(Stream: TStream);
     // Creates and returns an exact copy of the entry
    function  Clone: TDKLang_ResEntry;
     // Props
     // -- Characteristics
    property Characteristics: Cardinal read FCharacteristics write FCharacteristics;
     // -- Data version
    property DataVersion: Cardinal read FDataVersion write FDataVersion;
     // -- Language
    property Language: LANGID read FLanguage write FLanguage;
     // -- Memory flags
    property MemoryFlags: Word read FMemoryFlags write FMemoryFlags;
     // -- Entry name
    property Name: WideString read FName write FName;
     // -- Raw (unparsed) entry data
    property RawData: String read FRawData write FRawData;
     // -- Entry resource type
    property ResType: WideString read FResType write FResType;
     // -- Version
    property Version: Cardinal read FVersion write FVersion;
  end;

implementation //=======================================================================================================
uses TntDialogs;

   //===================================================================================================================
   // TDKLang_ResFile
   //===================================================================================================================

  function TDKLang_ResFile.AddEntry(Item: TDKLang_ResEntry): Integer;
  begin
    Result := FEntries.Add(Item);
  end;

  procedure TDKLang_ResFile.ClearEntries;
  begin
    FEntries.Clear;
  end;

  constructor TDKLang_ResFile.Create;
  begin
    inherited Create;
    FEntries := TObjectList.Create(True);
  end;

  procedure TDKLang_ResFile.DeleteEntry(Index: Integer);
  begin
    FEntries.Delete(Index);
  end;

  destructor TDKLang_ResFile.Destroy;
  begin
    FEntries.Free;
    inherited Destroy;
  end;

  function TDKLang_ResFile.FindEntry(const wsType, wsName: WideString): TDKLang_ResEntry;
  var i: Integer;
  begin
    for i := 0 to EntryCount-1 do begin
      Result := Entries[i];
      if WideSameText(Result.ResType, wsType) and WideSameText(Result.Name, wsName) then Exit;
    end;
    Result := nil;
  end;

  function TDKLang_ResFile.GetEntries(Index: Integer): TDKLang_ResEntry;
  begin
    Result := TDKLang_ResEntry(FEntries[Index]);
  end;

  function TDKLang_ResFile.GetEntryCount: Integer;
  begin
    Result := FEntries.Count;
  end;

  procedure TDKLang_ResFile.LoadFromFile(const wsFileName: WideString);
  var Stream: TStream;
  begin
    Stream := TTntFileStream.Create(wsFileName, fmOpenRead or fmShareDenyWrite);
    try
      LoadFromStream(Stream);
    finally
      Stream.Free;
    end;
  end;

  procedure TDKLang_ResFile.LoadFromStream(Stream: TStream);
  var
    pBuffer, pData: PByte;
    iBufferSize, iBytesLeft, iBlockSize: Integer;
    Header: TResResourceEntryHeader;

     // Retrieves a string or numeric identifier from the data and shifts the pointer appropriately
    function RetrieveIdentifier(var p: PByte): WideString;
    begin
       // Numeric ID
      if PWord(p)^=$ffff then begin
        Inc(p, SizeOf(Word));
        Result := IntToStr(PWord(p)^);
        Inc(p, SizeOf(Word))
       // A wide string name
      end else begin
        Result := WideString(PWideChar(p));
        Inc(p, (Length(Result)+1)*SizeOf(WideChar));
      end;
    end;

     // Processes a resource entry
    procedure ProcessResourceEntry;
    var
      p: PByte;
      wsName, wsType: WideString;
      EntryProps: TResResourceEntryProps;
      Entry: TDKLang_ResEntry;
      sRawData: String;
    begin
      p := pData;
       // Skip the header
      Inc(p, SizeOf(Header));
       // Retrieve resource type and name
      wsType := RetrieveIdentifier(p);
      wsName := RetrieveIdentifier(p);
       // Skip the dummy 32-bit indicator entry
      if (wsType<>'0') or (wsName<>'0') then begin
         // Align the pointer to a 4-byte boundary
        if (Integer(p) mod 4)<>0 then Inc(p, 4-Integer(p) mod 4);
         // Read entry properties
        Move(p^, EntryProps, SizeOf(EntryProps));
         // Create an entry
        Entry := TDKLang_ResEntry.Create;
        try
          Entry.ResType         := wsType;
          Entry.Name            := wsName;
          Entry.DataVersion     := EntryProps.cDataVersion;
          Entry.MemoryFlags     := EntryProps.wMemoryFlags;
          Entry.Language        := EntryProps.wLanguage;
          Entry.Version         := EntryProps.cVersion;
          Entry.Characteristics := EntryProps.cCharacteristics;
          SetString(sRawData, PChar(Integer(pData)+Header.iHeaderSize), Header.iDataSize);
          Entry.RawData         := sRawData;
           // Register the entry in the list
          AddEntry(Entry);
        except
          Entry.Free;
          raise;
        end;
      end;
    end;

  begin
     // Clear the entry list
    ClearEntries;
     // Allocate the buffer
    iBufferSize := Stream.Size;
    GetMem (pBuffer, iBufferSize);
    try
       // Read the entire file into the buffer
      Stream.ReadBuffer(pBuffer^, iBufferSize);
       // Scan the buffer
      iBytesLeft := iBufferSize;
      pData := pBuffer;
      while iBytesLeft>=SizeOf(Header) do begin
         // Read the header
        Move(pData^, Header, SizeOf(Header));
         // Process the entry
        ProcessResourceEntry;
         // Shift pointers
        iBlockSize := ((Header.iDataSize+Header.iHeaderSize+3) div 4)*4;
        Inc(pData,      iBlockSize);
        Dec(iBytesLeft, iBlockSize);
      end;
    finally
      FreeMem(pBuffer);
    end;
  end;

  function TDKLang_ResFile.RemoveEntry(Item: TDKLang_ResEntry): Integer;
  begin
    Result := FEntries.Remove(Item);
  end;

  procedure TDKLang_ResFile.SaveToFile(const wsFileName: WideString);
  var Stream: TStream;
  begin
    Stream := TTntFileStream.Create(wsFileName, fmCreate);
    try
      SaveToStream(Stream);
    finally
      Stream.Free;
    end;
  end;

  procedure TDKLang_ResFile.SaveToStream(Stream: TStream);
  var
    REIndicator: TDKLang_ResEntry;
    i: Integer;
  begin
     // Write dummy 32-bit resource indicator
    REIndicator := TDKLang_ResEntry.Create;
    try
      REIndicator.ResType := '0';
      REIndicator.Name    := '0';
      REIndicator.SaveToStream(Stream);
    finally
      REIndicator.Free;
    end;
     // Write real entries
    for i := 0 to EntryCount-1 do Entries[i].SaveToStream(Stream);
  end;

   //===================================================================================================================
   // TDKLang_ResEntry
   //===================================================================================================================

  function TDKLang_ResEntry.Clone: TDKLang_ResEntry;
  begin
    Result := TDKLang_ResEntry.Create;
    try
      Result.Characteristics := Characteristics;
      Result.DataVersion     := DataVersion;
      Result.Language        := Language;
      Result.MemoryFlags     := MemoryFlags;
      Result.Name            := Name;
      Result.RawData         := RawData;
      Result.ResType         := ResType;
      Result.Version         := Version;
    except
      Result.Free;
      raise;
    end;
  end;

  procedure TDKLang_ResEntry.SaveToStream(Stream: TStream);
  var
    msHeaderBlock: TMemoryStream;
    Header: TResResourceEntryHeader;
    Props: TResResourceEntryProps;

     // Writes a numeric or string identifier into the stream
    procedure WriteIdentifier(const wsID: WideString; Stream: TStream);
    var
      iNumericID: Integer;
      w: Word;
    begin
      iNumericID := StrToIntDef(wsID, -1);
       // String ID
      if iNumericID<0 then
        Stream.WriteBuffer(wsID[1], (Length(wsID)+1)*SizeOf(WideChar))
       // Numeric ID
      else begin
        w := $ffff;
        Stream.WriteBuffer(w, SizeOf(w));
        w := iNumericID;
        Stream.WriteBuffer(w, SizeOf(w));
      end;
    end;

     // Aligns the stream position to a 4-byte boundary 
    procedure AlignStream4(Stream: TStream);
    const IZero: Integer = 0;
    var iMod: Integer;
    begin
      iMod := Stream.Position mod 4;
      if iMod>0 then Stream.WriteBuffer(IZero, 4-iMod);
    end;

  begin
     // Prepare header block
    msHeaderBlock := TMemoryStream.Create;
    try
       // Write type and name identifiers
      WriteIdentifier(ResType, msHeaderBlock);
      WriteIdentifier(Name, msHeaderBlock);
       // Align the stream pointer
      AlignStream4(msHeaderBlock);
       // Fill properties record
      Props.cDataVersion     := DataVersion;
      Props.wMemoryFlags     := MemoryFlags;
      Props.wLanguage        := Language;
      Props.cVersion         := Version;
      Props.cCharacteristics := Characteristics;
       // Write properties
      msHeaderBlock.WriteBuffer(Props, SizeOf(Props));
       // Fill header record
      Header.iDataSize   := ((Length(FRawData)+3) div 4)*4;
      Header.iHeaderSize := msHeaderBlock.Size+SizeOf(Header);
       // Put the header record
      Stream.WriteBuffer(Header, SizeOf(Header));
       // Put the header block
      Stream.CopyFrom(msHeaderBlock, 0);
       // Put entry data
      Stream.WriteBuffer(RawData[1], Length(RawData));
       // Align the stream pointer
      AlignStream4(Stream);
    finally
      msHeaderBlock.Free;
    end;
  end;

end.
