{
    Маленький модуль для быстрого и простого чтения dpk паков.

    В текущей реализации грузит весь пак в память, что гарантирует
    быстрое чтение любого файла.

    Пакер для dpk реализован в модуле dpacker
    Утилитка в командной строке для работы с паками находится в dpack.pas4

    Применяется в фреймворке DEng (см. MountDojPack).

    Типичное применение:
      DojUnpacker.Init('pack.dpk');
      // по DojUnpacker.Error можно узнать была ли ошибка при инициализации
      if DojUnpacker.InitMemoryStream('file/inside/pack.dat', MemoryStream) then begin
        // Читаем файл из потока MemoryStream
      end else begin
        // Такого файла нет в паке
      end;
      // либо: Stream := DojUnpacker.CreateStream('file/inside/pack.dat')
      DojUnpacker.Done;
}
unit dunpacker;

interface

uses
  derror,
  dstream,
  dstreammem;

type
  T4BytesSignature = array[0..3] of AnsiChar;

const
  DOJ_PACK_MAGIC: T4BytesSignature = 'DPK'#10;

  DOJ_PACK_FORMAT_TXT: T4BytesSignature = 'txt'#10;
  DOJ_PACK_FORMAT_BIN: T4BytesSignature = 'bin'#10;

  DOJ_PACK_VERSION = 0;

type
TDojPackHeader = packed record
  Magic: T4BytesSignature; // DPK\n
  Format: T4BytesSignature; // txt\n or binary format
  Version: Cardinal;
  FileListOffset: Cardinal;
  FilesCount: Cardinal;
  ContentOffset: Cardinal;
end;

TDojPackFileInfo = record
  Path: AnsiString;
  Offset: Int64;
  Size: Int64;
end;

TDojUnpacker = object
public type
  TFileList = array of TDojPackFileInfo;
private
  FMemory: PByte;
  FSize: SizeUInt;
  FNeedFree: Boolean;

  FHeader: TDojPackHeader;
  FFileList: array of TDojPackFileInfo;
  FContentStart: PByte;

  FError: TError;

  // Returns -1 if not found
  function FindByPath(const Path: AnsiString): LongInt;
public
  constructor Init(const Data: TData);
  destructor Done; virtual;
  procedure Reinit(const Data: TData);
  // Returns nil if there is not such file inside the pack
  function CreateStream(const Path: AnsiString): PStreamMem;
  // Returns False if there is no such file inside the pack
  function InitMemoryStream(const Path: AnsiString; var StreamMem: TStreamMem): Boolean;

  procedure GetFileList(var FileList: TFileList);
  function Error: PError; inline;
end;

implementation

function TDojUnpacker.FindByPath(const Path: AnsiString): LongInt;
begin
  Result := 0;
  while Result < Length(FFileList) do begin
    if FFileList[Result].Path = Path then
      Exit;
    Inc(Result);
  end;
  Exit(-1);
end;

constructor TDojUnpacker.Init(const Data: TData);
label
  _Finally;
var
  Stream: TStreamMem;
  I: LongInt;
begin
  // Precharging all data to memory
  Data.Stream.ReadToMemory(FMemory, FSize);
  Data.Done;
  FNeedFree := True;

  // Read header
  Stream.Init(FMemory, FSize);
  Stream.Read(@FHeader, SizeOf(TDojPackHeader));
  if FHeader.Magic <> DOJ_PACK_MAGIC then begin
    SetupError(@FError, 1, 'it is not dpk file');
    goto _Finally;
  end;
  if FHeader.Magic <> DOJ_PACK_MAGIC then begin
    SetupError(@FError, 1, 'it is not dpk file');
    goto _Finally;
  end;
  if FHeader.Format <> DOJ_PACK_FORMAT_BIN then begin
    SetupError(@FError, 1, 'dpk format is not supported');
    goto _Finally;
  end;
  if FHeader.Version > DOJ_PACK_VERSION then begin
    SetupError(@FError, 1, 'dpk version is not supported');
    goto _Finally;
  end;

  // Read files list
  SetLength(FFileList, FHeader.FilesCount);
  for I := 0 to High(FFileList) do begin
    FFileList[I].Path := Stream.ReadStr;
    FFileList[I].Offset := Stream.ReadUint64;
    FFileList[I].Size := Stream.ReadUint64;
  end;

  // Compute content start
  FContentStart := FMemory + FHeader.ContentOffset;

  // All ok, exit
  SetupError(@FError, OK, '');

_Finally:
  Stream.Done;
end;

destructor TDojUnpacker.Done;
begin
  if FNeedFree then begin
    FreeMem(FMemory);
  end;
end;

procedure TDojUnpacker.Reinit(const Data: TData);
begin
  Done;
  Init(Data);
end;

function TDojUnpacker.CreateStream(const Path: AnsiString): PStreamMem;
var
  I: LongInt;
begin
  I := FindByPath(Path);
  if I = -1 then begin
    SetupError(@FError, 1, 'file ' + Path + ' not found in pack');
    Exit(nil);
  end;
  Exit(TStreamMem.Create(FContentStart + FFileList[I].Offset, FFileList[I].Size));
end;

function TDojUnpacker.InitMemoryStream(const Path: AnsiString; var StreamMem: TStreamMem): Boolean;
var
  I: LongInt;
begin
  I := FindByPath(Path);
  if I = -1 then
    Exit(SetupError(@FError, 1, 'file ' + Path + ' not found in pack'));
  StreamMem.Init(FContentStart + FFileList[I].Offset, FFileList[I].Size);
  Exit(True);
end;

procedure TDojUnpacker.GetFileList(var FileList: TFileList);
var
  I: LongInt;
begin
  SetLength(FileList, Length(FFileList));
  for I := 0 to High(FFileList) do
    FileList[I] := FFileList[I];
end;

function TDojUnpacker.Error: PError;
begin
  Result := @FError;
end;

end.
