Содержание
Запаковка ресурсов программы в ZIP архив, и работа с ним из памяти. Нативно из Delphi.
Концепт и статья
Люди которые программируют на Delphi очень давно уже ищут способ, каким образом можно сохранить ресурсы программы внутри самой программы, ну и желательно что-бы до файлов не было доступа из вне. Вот и я однажды об этом задумался, когда разберал программку написанную на Java. Ведь язык Java создаёт нативные пакеты jar которые по сути обычный zip файл. Содержащий в себе все ресурсы и бинарные исполняемые пакеты .class. Конечно в самом Java это понять можно, у них всё сконцентрировано на кроссплатформенность. То-есть логический пакет jar должен свободно запускаться как на суперкомпьютере так и на микроволновке. Конечно если программист который писал код подумал о том что я захочу его запустить скажем на кпк или той-же микроволновке. Ну ладно что-то я отошел от темы.
Итак задачи :
- Лёгкая компиляция архива .zip при компиляции кода delphi.
- Лёгкий импорт кода внутрь приложения.
- Быстрое чтение архива из памяти и возможность сохранения на диск (кэш, инсталлеры).
- Защита архива от шальных ручёнок и любопытных глаз.
Приступим :
Для начала я поднял мануалы по 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.