unit CnSMRBplUtils;

interface

{$I CnWizards.inc}

uses
  SysUtils, Windows, Classes, CnBaseUtils;

const
  csDefaultPackageExt = '.bpl';
  cssExecutableFiles = ' Executable Files ';
  cssUnits = 'Units:';
  cssRequiredPackages = 'Required Packages:';
  { DO NOT LOCALIZE }

  csFileNotExists = 'File %s not exists.';
  csCanNotAnalyseFile = 'Analyze "%s" failed: ';

type
  PPackageInfos = ^TPackageInfos;
  TPackageInfos = record
    Units: TStrings;
    RequiredPackages: TStrings;
  end;

  TPackageInfosList = class(TStringList)
  private
    FStringProcessProc: TStringProcessProc;
    function GetPackageInfos(Index: Integer): PPackageInfos;
    procedure SetPackageInfos(Index: Integer; const Value: PPackageInfos);

    function ProcessString(const s: string): string;

    procedure ClearPackageInfos;
    procedure InternalSaveToText(var Stream: Text);
  public
    constructor Create;
    destructor Destroy; override;

    function GetRequiredPackagesSectionName(const s: string): string; virtual;
    function GetUnitsSectionName(const s: string): string; virtual;

    procedure AddFile(const FileName: string);
    procedure Clear; override;
    procedure Delete(Index: Integer); override;

    procedure BuildPackageUsedBy(UsedByPackagesList: TStringObjectList); overload;
    procedure BuildPackageUsedBy(UsedByPackagesList: TStringObjectList; TempStrings: TStrings); overload;
    procedure BuildUnits(Units: TStringObjectList);
    procedure GetAllAffectedPackages(UsedByPackagesList: TStringObjectList; ssUsedByPackages, ssAllAffectedPackages: TStrings);
    procedure GetAllRequiredPackages(ssRequiredPackages, ssAllRequiredPackages: TStrings);
    procedure GetAllUsedByPackages(UsedByPackagesList: TStringObjectList; ssUsedByPackages, ssAllUsedByPackages: TStrings);
    procedure GetRequiredPackages(const s: string; ssAllRequiredPackages: TStrings);
    procedure GetUsedByPackages(UsedByPackagesList: TStringObjectList; const s: string; ssAllUsedByPackages: TStrings);

    procedure AppendToFile(const FileName: string);
    procedure LoadFromFile(const FileName: string); override;
    procedure SaveToFile(const FileName: string); override;

    property PackageInfos[Index: Integer]: PPackageInfos read GetPackageInfos write SetPackageInfos;
    property StringProcessProc: TStringProcessProc read FStringProcessProc write FStringProcessProc;
  end;

function NewPackageInfos: PPackageInfos;
function NewPackageInfosAndCreateList: PPackageInfos;
procedure DisposePackageInfos(P: PPackageInfos);

function EnumUnits(const FileName: string; ss: TStrings): Boolean; overload;
function EnumUnits(const Module: HMODULE; ss: TStrings): Boolean; overload;
function EnumRequiredPackages(const FileName: string; ss: TStrings): Boolean; overload;
function EnumRequiredPackages(const Module: HMODULE; ss: TStrings): Boolean; overload;
function EnumPackageInfos(const FileName: string; ssUnits, ssRequiredPackages: TStrings): Boolean; overload;
function EnumPackageInfos(const Module: HMODULE; ssUnits, ssRequiredPackages: TStrings): Boolean; overload;

implementation

{$IFDEF DELPHI7_UP}
  {$WARN SYMBOL_PLATFORM OFF}
  {$WARN UNIT_PLATFORM OFF}
{$ENDIF}

uses
  FileCtrl, CnBuffStr, CnSMRPEUtils;

function NewPackageInfos: PPackageInfos;
begin
  New(Result);
  Result.Units := nil;
  Result.RequiredPackages := nil;
end;

function NewPackageInfosAndCreateList: PPackageInfos;
begin
  New(Result);
  Result.Units := TStringList.Create;
  TStringList(Result.Units).Sorted := True;
  Result.RequiredPackages := TStringList.Create;
  TStringList(Result.RequiredPackages).Sorted := True;
end;

procedure DisposePackageInfos(P: PPackageInfos);
begin
  if P <> nil then
  begin
    if Assigned(P.Units) then
    begin
      FreeAndNil(P.Units);
    end;
    if Assigned(P.RequiredPackages) then
    begin
      FreeAndNil(P.RequiredPackages);
    end;
    Dispose(P);
  end;
end;

procedure GetPackageInfosProc(const Name: string; NameType: TNameType; Flags: Byte;
  Param: Pointer);
var
  P: PPackageInfos;
begin
  P := PPackageInfos(Param);
  if not Assigned(P) then
  begin
    Exit;
  end;

  if (NameType = ntContainsUnit) and (Assigned(P.Units)) then
  begin
    P.Units.Add(Name);
  end
  else if (NameType = ntRequiresPackage) and (Assigned(P.RequiredPackages)) then
  begin
    P.RequiredPackages.Add(Name);
  end;
end;

function EnumUnits(const FileName: string; ss: TStrings): Boolean;
begin
  Result := EnumPackageInfos(FileName, ss, nil);
end;

function EnumUnits(const Module: HMODULE; ss: TStrings): Boolean;
begin
  Result := EnumPackageInfos(Module, ss, nil);
end;

function EnumRequiredPackages(const FileName: string; ss: TStrings): Boolean;
begin
  Result := EnumPackageInfos(FileName, nil, ss);
end;

function EnumRequiredPackages(const Module: HMODULE; ss: TStrings): Boolean;
begin
  Result := EnumPackageInfos(Module, nil, ss);
end;

function EnumPackageInfos(const FileName: string; ssUnits, ssRequiredPackages: TStrings): Boolean;
var
  Module: HMODULE;
begin
  Result := False;
  if not FileExists(FileName) then
  begin
    Exit;
  end;

  Module := LoadLibraryEx(PChar(FileName), 0, LOAD_LIBRARY_AS_DATAFILE);
  if Module = 0 then
  begin
    RaiseLastWin32Error;
  end;
  
  try
    Result := EnumPackageInfos(Module, ssUnits, ssRequiredPackages);
  finally
    FreeLibrary(Module);
  end;
end;

function EnumPackageInfos(const Module: HMODULE; ssUnits, ssRequiredPackages: TStrings): Boolean;
var
  Flags: Integer;
  PI: TPackageInfos;
begin
  Result := False;
  if FindResource(Module, 'PACKAGEINFO', RT_RCDATA) <> 0 then
  begin
    PI.Units := ssUnits;
    PI.RequiredPackages := ssRequiredPackages;
    GetPackageInfo(Module, @PI, Flags, GetPackageInfosProc);
    Result := True;
  end;
end;

{ TPackageInfosList }

procedure TPackageInfosList.AddFile(const FileName: string);
var
  P: PPackageInfos;
begin
  if not FileExists(FileName) then
  begin
    raise Exception.CreateFmt(csFileNotExists, [AnsiQuotedStr(FileName, '"')]);
  end;

  P := NewPackageInfosAndCreateList;
  try
    EnumPackageInfos(FileName, P.Units, P.RequiredPackages);
    if P.RequiredPackages.Count = 0 then
      GetImportTable(P.RequiredPackages, FileName);
  except
    on E: Exception do
    begin
      DisposePackageInfos(P);
      E.Message := Format(csCanNotAnalyseFile, [FileName]) + E.Message;
      raise;
    end;
  end;
  AddObject(FileName, Pointer(P));
end;

procedure TPackageInfosList.AppendToFile(const FileName: string);
var
  Stream: TextFile;
begin
  AssignFile(Stream, FileName);
  try
    if FileExists(FileName) then
    begin
      System.Append(Stream);
    end
    else
    begin
      Rewrite(Stream);
    end;
    InternalSaveToText(Stream);
  finally
    CloseFile(Stream);
  end;
end;

procedure TPackageInfosList.BuildPackageUsedBy(
  UsedByPackagesList: TStringObjectList; TempStrings: TStrings);
var
  i, j: Integer;
  idx: Integer;
  P: PPackageInfos;
  sRequiredPackage, sPackages, s: string;
begin
  if not (Assigned(UsedByPackagesList) and Assigned(TempStrings)) then
  begin
    Exit;
  end;

  UsedByPackagesList.BeginUpdate;
  try
    UsedByPackagesList.Clear;
    for i := 0 to Self.Count - 1 do
    begin
      P := Self.PackageInfos[i];
      if (P <> nil) and (P.RequiredPackages <> nil) then
      begin
        for j := 0 to P.RequiredPackages.Count - 1 do
        begin
          sRequiredPackage := P.RequiredPackages[j];
          if ExtractFileExt(sRequiredPackage) = '' then
          begin
            sRequiredPackage := sRequiredPackage + csDefaultPackageExt;
          end;
          with UsedByPackagesList do
          begin
            idx := IndexOf(sRequiredPackage);
            if idx >= 0 then
            begin
              s := Strings[idx];
              SetCommaText(StringObjects[idx], TempStrings);
              TempStrings.Add(Self[i]);
              sPackages := TempStrings.CommaText;
              Delete(idx);
            end
            else
            begin
              sPackages := AnsiQuotedStr(Self[i], '"');
            end;
            AddStringObject(sRequiredPackage, sPackages);
          end;
        end;
      end;
    end;
  finally
    UsedByPackagesList.EndUpdate;
  end;
end;

procedure TPackageInfosList.BuildUnits(Units: TStringObjectList);
var
  i, j: Integer;
  idx: Integer;
  P: PPackageInfos;
  sUnit, sPackages, s: string;
begin
  Units.BeginUpdate;
  try
    Units.Clear;
    for i := 0 to Self.Count - 1 do
    begin
      P := Self.PackageInfos[i];
      if (P <> nil) and (P.Units <> nil) then
      begin
        for j := 0 to P.Units.Count - 1 do
        begin
          sUnit := P.Units[j];
          with Units do
          begin
            idx := IndexOf(sUnit);
            if idx >= 0 then
            begin
              s := Strings[idx];
              sPackages := StringObjects[idx] + ',' + AnsiQuotedStr(Self[i], '"');
              Delete(idx);
            end
            else
            begin
              sPackages := AnsiQuotedStr(Self[i], '"');
            end;
            AddStringObject(sUnit, sPackages);
          end;
        end;
      end;
    end;
  finally
    Units.EndUpdate;
  end;
end;

procedure TPackageInfosList.BuildPackageUsedBy(UsedByPackagesList: TStringObjectList);
var
  sstmp: TStringList;
begin
  sstmp := TStringList.Create;
  try
    sstmp.Sorted := True;
    BuildPackageUsedBy(UsedByPackagesList, sstmp);
  finally
    sstmp.Free;
  end;
end;

procedure TPackageInfosList.Clear;
begin
  ClearPackageInfos;
  inherited;
end;

procedure TPackageInfosList.ClearPackageInfos;
var
  i: Integer;
begin
  for i := 0 to Count - 1 do
  begin
    DisposePackageInfos(PPackageInfos(Objects[i]));
  end;
end;

constructor TPackageInfosList.Create;
begin
  inherited;
end;

procedure TPackageInfosList.Delete(Index: Integer);
begin
  if (Index < 0) or (Index >= Count) then
  begin
    Exit;
  end;
  DisposePackageInfos(PackageInfos[Index]);
  inherited;
end;

destructor TPackageInfosList.Destroy;
begin
  Clear;
  inherited;
end;

procedure TPackageInfosList.GetAllAffectedPackages(
  UsedByPackagesList: TStringObjectList;
  ssUsedByPackages, ssAllAffectedPackages: TStrings);
var
  i, j, idx: Integer;
  ss: TStringList;
begin
  if not (Assigned(ssUsedByPackages) and Assigned(ssAllAffectedPackages) and
    Assigned(UsedByPackagesList)) then
  begin
    Exit;
  end;

  ssAllAffectedPackages.Clear;
  for i := 0 to ssUsedByPackages.Count - 1 do
  begin
    ssAllAffectedPackages.Add(ssUsedByPackages[i]);
  end;

  ss := TStringList.Create;
  try
    for i := 0 to ssUsedByPackages.Count - 1 do
    begin
      idx := UsedByPackagesList.IndexOf(ssUsedByPackages[i]);
      if idx >= 0 then
      begin
        SetCommaText(UsedByPackagesList.StringObjects[idx], ss);
        for j := 0 to ss.Count - 1 do
        begin
          if ssAllAffectedPackages.IndexOf(ss[j]) < 0 then
            ssAllAffectedPackages.Add(ss[j]);
        end;
      end;
    end;
  finally
    ss.Free;
  end;
end;

procedure TPackageInfosList.GetAllRequiredPackages(ssRequiredPackages,
  ssAllRequiredPackages: TStrings);
var
  i: Integer;
  s: string;
begin
  if not (Assigned(ssRequiredPackages) and Assigned(ssAllRequiredPackages)) then
  begin
    Exit;
  end;

  for i := 0 to ssRequiredPackages.Count - 1 do
  begin
    s := ssRequiredPackages[i];
    if ssAllRequiredPackages.IndexOf(s) >= 0 then
    begin
      Continue;
    end;

    ssAllRequiredPackages.Add(s);
    GetRequiredPackages(s, ssAllRequiredPackages);
  end;
end;

procedure TPackageInfosList.GetAllUsedByPackages(UsedByPackagesList: TStringObjectList;
  ssUsedByPackages, ssAllUsedByPackages: TStrings);
var
  i: Integer;
  s: string;
begin
  if not (Assigned(ssUsedByPackages) and Assigned(ssAllUsedByPackages) and Assigned(UsedByPackagesList)) then
  begin
    Exit;
  end;

  for i := 0 to ssUsedByPackages.Count - 1 do
  begin
    s := ssUsedByPackages[i];
    if ssAllUsedByPackages.IndexOf(s) >= 0 then
    begin
      Continue;
    end;

    ssAllUsedByPackages.Add(s);
    GetUsedByPackages(UsedByPackagesList, s, ssAllUsedByPackages);
  end;
end;

function TPackageInfosList.GetPackageInfos(Index: Integer): PPackageInfos;
begin
  if (Index < 0) or (Index >= Count) then
  begin
    Result := nil;
    Exit;
  end;
  Result := Pointer(Objects[Index]);
end;

function TPackageInfosList.GetRequiredPackagesSectionName(
  const s: string): string;
begin
  Result := cssRequiredPackages + s;
end;

procedure TPackageInfosList.GetRequiredPackages(const s: string;
  ssAllRequiredPackages: TStrings);
var
  P: PPackageInfos;
begin
  P := Self.PackageInfos[Self.IndexOf(s)];
  if not (Assigned(ssAllRequiredPackages) and Assigned(P)) then
  begin
    Exit;
  end;

  GetAllRequiredPackages(P.RequiredPackages, ssAllRequiredPackages);
end;

function TPackageInfosList.GetUnitsSectionName(const s: string): string;
begin
  Result := cssUnits + s;
end;

procedure TPackageInfosList.GetUsedByPackages(UsedByPackagesList: TStringObjectList;
  const s: string; ssAllUsedByPackages: TStrings);
var
  idx: Integer;
  ss: TStringList;
begin
  if not (Assigned(ssAllUsedByPackages) and Assigned(UsedByPackagesList)) then
  begin
    Exit;
  end;

  idx := UsedByPackagesList.IndexOf(s);
  if idx < 0 then
  begin
    Exit;
  end;

  ss := TStringList.Create;
  try
    SetCommaText(UsedByPackagesList.StringObjects[idx], ss);
    GetAllUsedByPackages(UsedByPackagesList, ss, ssAllUsedByPackages);
  finally
    ss.Free;
  end;
end;

procedure TPackageInfosList.InternalSaveToText(var Stream: Text);
var
  i: Integer;
begin
  //Save Head
  StringsSaveToTextWithSection(Self, Stream, cssExecutableFiles, False, StringProcessProc);
  //Save Results
  for i := 0 to Count - 1 do
  begin
    if PackageInfos[i] <> nil then
    begin
      with PackageInfos[i]^ do
      begin
        StringsSaveToTextWithSection(
          Units,
          Stream,
          GetUnitsSectionName(ProcessString(Strings[i])));
        StringsSaveToTextWithSection(
          RequiredPackages,
          Stream,
          GetRequiredPackagesSectionName(ProcessString(Strings[i])));
      end;
    end;
  end;
end;

procedure TPackageInfosList.LoadFromFile(const FileName: string);
var
  P: PPackageInfos;
  Stream: TStringReader;
  i: Integer;
  sl: TSectionList;
begin
  Stream := TStringReader.Create;
  try
    Stream.LoadFromFile(FileName);
    BulidSectionList(Stream, sl);
//    sl := nil;
    BeginUpdate;
    try
      Clear;
      //Load Head
      StringsLoadFromTextWithSection(Self, Stream, cssExecutableFiles, sl);
      //Load Results
      for i := 0 to Count - 1 do
      begin
        P := NewPackageInfosAndCreateList;
        try
          PackageInfos[i] := P;
          StringsLoadFromTextWithSection(P.Units,
            Stream,
            GetUnitsSectionName(Strings[i]),
            sl);
          StringsLoadFromTextWithSection(
            P.RequiredPackages,
            Stream,
            GetRequiredPackagesSectionName(Strings[i]),
            sl);
        except
          DisposePackageInfos(P);
          raise;
        end;
      end;
    finally
      EndUpdate;
    end;
  finally
    Stream.Free;
    FreeSectionList(sl);
  end;
end;

function TPackageInfosList.ProcessString(const s: string): string;
begin
  if Assigned(StringProcessProc) then
  begin
    Result := StringProcessProc(s);
  end
  else
  begin
    Result := s;
  end;
end;

procedure TPackageInfosList.SaveToFile(const FileName: string);
var
  Stream: TextFile;
begin
  AssignFile(Stream, FileName);
  try
    Rewrite(Stream);
    InternalSaveToText(Stream);
  finally
    CloseFile(Stream);
  end;
end;

procedure TPackageInfosList.SetPackageInfos(Index: Integer;
  const Value: PPackageInfos);
begin
  if (Index < 0) or (Index >= Count) then
  begin
    Exit;
  end;
  PutObject(Index, Pointer(Value));
end;

end.
