{
    ReadZIP содержит класс для чтения архивов в формате ZIP.

    Copyright © 2016, 2019, 2022 Малик Разработчик

    Это свободная программа: вы можете перераспространять её и/или
    изменять её на условиях Меньшей Стандартной общественной лицензии GNU в том виде,
    в каком она была опубликована Фондом свободного программного обеспечения;
    либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.

    Эта программа распространяется в надежде, что она может быть полезна,
    но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
    или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЁННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
    общественной лицензии GNU.

    Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
    вместе с этой программой. Если это не так, см.
    <http://www.gnu.org/licenses/>.
}

unit ReadZIP;

{$MODE DELPHI}

interface

uses
    Lang,
    IOStreams,
    Zlib;

{%region public }
type
    ZIPArchiveReader = class(_Object)
    strict private
        onEnd: boolean;
        start: long;
        archive: DataInput;
        procedure checkEnd();
        function getFileSizes(): long;
    public
        constructor create(source: Input);
        procedure gotoFirstFile();
        procedure gotoNextFile();
        function isStayOnEnd(): boolean;
        function openFile(const fileName: AnsiString): Input;
        function openCurrentFile(): Input;
        function getCurrentFileName(): AnsiString;

    strict private const
        INFO_TYPE_FILE = int($04034b50);
        INFO_TYPE_FILE_DESCRIPTOR = int($08074b50);
        INFO_TYPE_EXTRA_DATA = int($08064b50);
        INFO_TYPE_CENTRAL_DIRECTORY = int($02014b50);
        INFO_TYPE_DIGITAL_SIGNATURE = int($05054b50);
        INFO_TYPE_END_OF_CENTRAL_DIRECTORY = int($06054b50);
        DESCRIPTOR_SIZE = int($10);
        METHOD_NO_COMPRESSION = int(0);
        METHOD_DEFLATED = int(8);
    end;
{%endregion}

implementation

{%region ZIPArchiveReader }
    procedure ZIPArchiveReader.checkEnd();
    var
        archive: DataInput;
        i: int;
    begin
        archive := self.archive;
        try
            while archive.available() > 0 do begin
                case archive.readIntLE() of
                INFO_TYPE_FILE: begin
                    onEnd := false;
                    exit;
                end;
                INFO_TYPE_FILE_DESCRIPTOR: begin
                    archive.seek($0c);
                end;
                INFO_TYPE_EXTRA_DATA: begin
                    i := archive.readIntLE();
                    archive.seek(zeroExtend(i));
                end;
                INFO_TYPE_CENTRAL_DIRECTORY: begin
                    archive.seek($18);
                    i := archive.readUnsignedShortLE() + archive.readUnsignedShortLE() + archive.readUnsignedShortLE() + $0c;
                    archive.seek(long(i));
                end;
                INFO_TYPE_DIGITAL_SIGNATURE: begin
                    i := archive.readUnsignedShortLE();
                    archive.seek(long(i));
                end;
                INFO_TYPE_END_OF_CENTRAL_DIRECTORY: begin
                    archive.seek($10);
                    i := archive.readUnsignedShortLE();
                    archive.seek(long(i));
                end;
                else
                    break;
                end;
            end;
            onEnd := true;
        except
            on e: IOException do begin
                onEnd := true;
            end;
            else begin
                raise;
            end;
        end;
    end;

    function ZIPArchiveReader.getFileSizes(): long;
    var
        archive: DataInput;
        readed: int;
        count: int;
        i: int;
        s: int;
        bytes: byte_Array1d;
    begin
        result := 0;
        archive := self.archive;
        bytes := byte_Array1d_create(2 * DESCRIPTOR_SIZE);
        readed := archive.read(bytes, 0, DESCRIPTOR_SIZE);
        repeat
            count := archive.read(bytes, DESCRIPTOR_SIZE, DESCRIPTOR_SIZE);
            inc(readed, count);
            for i := 0 to count - 1 do begin
                s := i + readed - count - DESCRIPTOR_SIZE;
                if (int((@(bytes[i]))^) = INFO_TYPE_FILE_DESCRIPTOR) and (int((@(bytes[i + $08]))^) = s) then begin
                    archive.seek(long(-readed));
                    result := buildLong(int((@(bytes[i + $08]))^), int((@(bytes[i + $0c]))^));
                    exit;
                end;
            end;
            arraycopy(bytes, DESCRIPTOR_SIZE, bytes, 0, DESCRIPTOR_SIZE);
        until false;
    end;

    constructor ZIPArchiveReader.create(source: Input);
    begin
        inherited create();
        self.onEnd := true;
        self.archive := DataInputStream.create(source);
        if source <> nil then begin
            self.start := source.position();
            checkEnd();
        end;
    end;

    procedure ZIPArchiveReader.gotoFirstFile();
    var
        archive: Input;
    begin
        archive := self.archive;
        archive.seek(start - archive.position());
        checkEnd();
    end;

    procedure ZIPArchiveReader.gotoNextFile();
    var
        flags: int;
        compressedSize: int;
        nameExtraLength: int;
        archive: DataInput;
    begin
        if onEnd then begin
            exit;
        end;
        archive := self.archive;
        archive.seek($02);
        flags := archive.readUnsignedShortLE();
        archive.seek($0a);
        compressedSize := archive.readIntLE();
        archive.seek($04);
        nameExtraLength := archive.readUnsignedShortLE() + archive.readUnsignedShortLE();
        archive.seek(long(nameExtraLength));
        if ((flags and $08) <> 0) and (compressedSize = 0) then begin
            compressedSize := int(getFileSizes());
        end;
        archive.seek(zeroExtend(compressedSize));
        checkEnd();
    end;

    function ZIPArchiveReader.isStayOnEnd(): boolean;
    begin
        result := onEnd;
    end;

    function ZIPArchiveReader.openFile(const fileName: AnsiString): Input;
    begin
        gotoFirstFile();
        while (not onEnd) and (getCurrentFileName() <> fileName) do begin
            gotoNextFile();
        end;
        if not onEnd then begin
            result := openCurrentFile();
        end else begin
            result := nil;
        end;
    end;

    function ZIPArchiveReader.openCurrentFile(): Input;
    var
        archive: DataInput;
        flags: int;
        compressionMethod: int;
        compressedSize: int;
        decompressedSize: int;
        nameExtraLength: int;
        fileSizes: long;
        fileContents: byte_Array1d;
    begin
        if onEnd then begin
            result := nil;
            exit;
        end;
        archive := self.archive;
        archive.seek($02);
        flags := archive.readUnsignedShortLE();
        compressionMethod := archive.readUnsignedShortLE();
        archive.seek($08);
        compressedSize := archive.readIntLE();
        decompressedSize := archive.readIntLE();
        nameExtraLength := archive.readUnsignedShortLE() + archive.readUnsignedShortLE();
        archive.seek(nameExtraLength);
        if ((flags and $08) <> 0) and (compressedSize = 0) then begin
            fileSizes := getFileSizes();
            compressedSize := int(fileSizes);
            decompressedSize := int(fileSizes shr 32);
        end;
        if ((flags and $01) = 0) and ((compressionMethod = METHOD_NO_COMPRESSION) or (compressionMethod = METHOD_DEFLATED)) and (compressedSize >= 0) and (decompressedSize >= 0) then begin
            setLength(fileContents, compressedSize);
            archive.read(fileContents);
            if compressionMethod = METHOD_DEFLATED then begin
                fileContents := Zlib.decompress(fileContents, false);
            end;
            result := ByteArrayInputStream.create(fileContents, 0, length(fileContents));
        end else begin
            archive.seek(zeroExtend(compressedSize));
            result := nil;
        end;
        checkEnd();
    end;

    function ZIPArchiveReader.getCurrentFileName(): AnsiString;
    var
        archive: DataInput;
        len: int;
        i: int;
    begin
        if onEnd then begin
            result := '';
            exit;
        end;
        archive := self.archive;
        archive.seek($16);
        len := archive.readUnsignedShortLE();
        archive.seek($02);
        result := String_create(len);
        for i := 1 to len do begin
            result[i] := char(archive.read());
        end;
        archive.seek(long(-(len + $1a)));
    end;
{%endregion}

end.

