works:programmer:delphi:binary-zip-resource

Запаковка ресурсов программы в ZIP архив, и работа с ним из памяти. Нативно из Delphi.

Концепт и статья

Люди которые программируют на Delphi очень давно уже ищут способ, каким образом можно сохранить ресурсы программы внутри самой программы, ну и желательно что-бы до файлов не было доступа из вне. Вот и я однажды об этом задумался, когда разберал программку написанную на Java. Ведь язык Java создаёт нативные пакеты jar которые по сути обычный zip файл. Содержащий в себе все ресурсы и бинарные исполняемые пакеты .class. Конечно в самом Java это понять можно, у них всё сконцентрировано на кроссплатформенность. То-есть логический пакет jar должен свободно запускаться как на суперкомпьютере так и на микроволновке. Конечно если программист который писал код подумал о том что я захочу его запустить скажем на кпк или той-же микроволновке. Ну ладно что-то я отошел от темы.

Итак задачи :

  1. Лёгкая компиляция архива .zip при компиляции кода delphi.
  2. Лёгкий импорт кода внутрь приложения.
  3. Быстрое чтение архива из памяти и возможность сохранения на диск (кэш, инсталлеры).
  4. Защита архива от шальных ручёнок и любопытных глаз.

Приступим :

Для начала я поднял мануалы по Delphi XE и нашел в них нативный компонент для работы с zip архивом. Уж ей богу Borland и CodeGear отличные компании которые создали максимально доступный и очень функциональный язык в котором есть практический всё в одном пакете. Купил, поставил, работаешь. Поэтому я не сторонник качать кучу библиотек из интернета и потом непонятно как она работает, что в ней реализовано, что нет, и каждый раз платить за платные версии дописанных библиотек плагинов и.т.п. Вообщем класс называется TZipFile и лежит он в библиотеке System.Zip. Итак изучаем мануал, конструктор у него обычный TZipFile.Create() без параметров что поможет загружать-выгружать его через try/finally. А ещё он имеет в себе функцию TZipFile.Open которая уже гораздо интереснее. Функция Open перегруженная (overload) и имеет 2 вида. 1й это загрузить zip из файла (const ZipFileName: string; OpenMode: TZipMode), но нас интересует больше 2й способ - загрузить zip из потока (ZipFileStream: TStream; OpenMode: TZipMode). Где первый параметр у нас сам поток, смею предположить что в моём случае будет TMemoryStream, а второй параметр это режим открытия файла в нашем случае открыть файл на чтение zmRead ( type TZipMode = (zmClosed, zmRead, zmReadWrite, zmWrite)).

Так-же есть функции

  • распаковки - TZipFile.Extract и TZipFile.ExtractAll
  • чтения заголовков - TZipFile.GetFileCount, TZipFile.GetFileInfo, TZipFile.GetFileName
  • и сохранение распакованных данных в поток TStream или массив байтов TBytes - TZipFile.Read.

По поводу как спрятать архив, идеи следующие. Ресурсный файл отпадает автоматом. Уже лет 10 есть программа Resource Hacker (кстати тоже написана на Delphi). Которая совершенно свободно распаковывает, запаковывает и изменяет любой ресурс приложения. А этого-бы очень не хотелось. Была так-же идея добавить zip в конце кода exe и просто подправить контрольную сумму файла CRC. Что конечно-же гемор. Не буду-же я при каждой компиляции открывать PE редактор и менять CRC. Тут я было чуток огорчился, но на помощь пришла гениальная идея! Когда-то очень давно наши отцы и деды писали на паскале и держали заготовленные файлы в константах. Константа неотъемлемая часть языка, загружается с программой непосредственно в память приложения и нагрузку на исходный код кроме как компиляции и времени на запуск приложения не несёт. Из чего этот вариант очень приемлем для увеличения безопасности, но бьёт по скорости загрузки приложения. Итак если в архиве нечего важного нету, то используем TResourceStream и кладём архив в файл .res через brcc32. Если нам нужна безопасность то есть скриптик написанный на PHP, который сделает из zip константу для Delphi.

complie-zip2inc.phpexec
<?php
 
# Открыть исходный файл
$src = fopen('Resources.zip', 'rb');
 
 
function convert2pascal($buffer)
{
    // "\0\0\0\0"   =>   "$00, $00, $00, $00"
    return '$' . substr( chunk_split(bin2hex($buffer), 2, ', $'), 0, -3);
}
 
if ( $src !== false )
{
 
    # Получить размер файла
    fseek($src, 0, SEEK_END);
    $src_size = ftell($src);
    fseek($src, 0, SEEK_SET);
 
    # Открыть файл получатель
    $dst = fopen('include_me.inc', 'wb');
    if ( $dst !== false )
    {
 
        fwrite($dst, 'const buffer_size = '. $src_size .";\r\n");
        fwrite($dst, 'const buffer: array[0..'. ($src_size - 1) .'] of Byte = ('."\r\n");
 
        $buffsz = $src_size;
        while ( $src_size > 0 )
        {
            $buffsz = $src_size > 80 ? 80 : $src_size;
            $buffer = fread($src, $buffsz);
            $buffer = convert2pascal($buffer);
 
            fwrite($dst, $buffer);
            $src_size -= $buffsz;
 
            if ( $src_size > 0 ) fwrite($dst, ",\r\n");
 
        }
 
        fwrite($dst, "\r\n);");
 
    }
 
    fclose($src);
}

Что делает этот код - берёт файл Resources.zip и делает из него файл include_me.inc для Delphi.

Сожержимое файла примерно следующее.

include_me.inc
const buffer_size = 56548;
const buffer: array[0..56547] of Byte = (
$50, $4b, $03, $04, $0a, $00, $00, $00, $00, $00, $b5, $60, $0c, $49, $00, $00,
$00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $0a, $00, $00, $00, $52, $65,
$73, $6f, $75, $72, $63, $65, $73, $2f  // ... и так далее до 56547го байта ...
);

Теперь у нас есть файл который мы можем добавить через дерективу компелятора Include - {$I 'include_me.inc'}

Имея бинарные данные нам надо сделать из них TMemoryStream. Как мы можем это сделать. Например просто создать TMemoryStream и скопировать блок памяти в блок памяти TMemoryStream.

PMem := TMemoryStream.Create;  /// Создаём поток памяти
try
    PMem.SetSize(BufferSize); /// Размер потока = Размеру нашего буффера
    Move(@buffer[0]^, PMem.Memory^, BufferSize);  // Скопировать Наш буффер в буффер памяти TMemoryStream на размер буффера байт
    // Хочу отдельно заметить PMem.Memory = @buffer    недопустимая ошибка!  Удалив изначалььный адрес в TMemoryStream.Memory создаст нехилую утечку памяти
    // Некогда не делайте так когда вы работаете с указателями (Pointers)
 
   // Тут мы можем что-то сделать с нашим потоком в котором лежит наш zip файл целиком.
 
finally
  FreeAndNil(PMem);
end

Ну а теперь можем передать поток первым параметром самому TZipFile.Open и работать с ним как с обычным архивом.

PZip := TZipFile.Create();
try
   PZip.Open(PMem, zmRead);
   // Тут работаем как с обычным zip файлом.
finally
   FreeAndNil(PZip);
end;

Исходный код программы

Сам исходный код примера программы, написанный на Delphi XE. Быстро топорно и на коленке. Просто проверить как эта хрень работает.

TestProject.dpr
program TestProject;
{$APPTYPE CONSOLE}
{$R *.res}
 
{$DEFINE INC_FROM_FILE}
 
uses
  System.SysUtils,
  System.Classes,
  System.Zip;
 
{ LZ Data }
 
{$IFDEF INC_FROM_FILE}
{$INCLUDE 'include_me.inc'}
{$ELSE}
const buffer_size = 2;
const buffer: array[0..1] of Byte = ($00, $00);
{$ENDIF}
 
 
type
    TLZBin = class(TObject)
    private
        PMem : TMemoryStream;
        PZip : TZipFile;
    public
        constructor Create(BaseAddr: Pointer; BufferSize: Int64);
        destructor Destroy; override;
        function HaveFile(FileName: string): Boolean;
        function FileToStream(FileName: string; WriteToStream: TStream): Boolean;
    end;
 
{ TLZBin }
 
constructor TLZBin.Create(BaseAddr: Pointer; BufferSize: Int64);
begin
    inherited Create();
    PMem := TMemoryStream.Create;
    PMem.SetSize(BufferSize);
    Move(BaseAddr^, PMem.Memory^, BufferSize);
    PZip := TZipFile.Create();
    PZip.Open(PMem, zmRead);
end;
 
destructor TLZBin.Destroy;
begin
    FreeAndNil(PZip);
    FreeAndNil(PMem);
    inherited;
end;
 
function TLZBin.FileToStream(FileName: string; WriteToStream: TStream): Boolean;
var
    DecompressionStream: TStream;
    LocalHeader: TZipHeader;
begin
    Result := False;
    Assert(Assigned(WriteToStream), '2nd param must be defined first');
    PZip.Read(FileName, DecompressionStream, LocalHeader);
    try
        WriteToStream.CopyFrom(DecompressionStream, DecompressionStream.Size);
        Result := True;
    finally
        DecompressionStream.Free;
    end;
end;
 
function TLZBin.HaveFile(FileName: string): Boolean;
begin
    Result := PZip.IndexOf(FileName) > -1;
end;
 
{ $EXE }
 
var lz: TLZBin;
    file_contents: TStringStream;
 
begin
    try
        lz := TLZBin.Create(@buffer[0], buffer_size);
        try
            if lz.HaveFile('Resources/Kbits.txt') then
            begin
                Writeln('File Exists, unpacking...');
 
                // read an file
                file_contents := TStringStream.Create();
                try
                    lz.FileToStream('Resources/Kbits.txt', file_contents);
                    file_contents.Seek(0,0);
                    Writeln(file_contents.ReadString(file_contents.Size));
                finally
                    FreeAndNil(file_contents);
                end;
                // end of read
            end
            else
                Writeln('File Not Exists');
        finally
            FreeAndNil(lz);
        end;
    except on E: Exception do
        Writeln(E.ClassName, ': ', E.Message);
    end;
end.

Исходный код архивчиком для скачивания

works/programmer/delphi/binary-zip-resource.txt · Last modified: 2018/12/15 17:18 (external edit)