Как я могу читать из самораспаковывающегося zip-файла Winzip (exe) на Java?

#java #zip

#java #застежка — молния

Вопрос:

Существует ли существующий метод или мне нужно будет вручную проанализировать и пропустить exe-блок перед передачей данных в ZipInputStream?

Ответ №1:

После просмотра формата EXE-файла и формата ZIP-файла и тестирования различных опций кажется, что самым простым решением является просто игнорировать любую преамбулу вплоть до первого заголовка локального zip-файла.

Макет Zip-файла

Заголовок локального zip-файла

Я написал фильтр входного потока, чтобы обойти преамбулу, и он работает отлично:

 ZipInputStream zis = new ZipInputStream(
    new WinZipInputStream(
    new FileInputStream("test.exe")));
while ((ze = zis.getNextEntry()) != null) {
    . . .
    zis.closeEntry();
}
zis.close();
 

WinZipInputStream.java

 import java.io.FilterInputStream;
import java.io.InputStream;
import java.io.IOException;

public class WinZipInputStream extends FilterInputStream {
    public static final byte[] ZIP_LOCAL = { 0x50, 0x4b, 0x03, 0x04 };
    protected int ip;
    protected int op;

    public WinZipInputStream(InputStream is) {
        super(is);
    }

    public int read() throws IOException {
        while(ip < ZIP_LOCAL.length) {
            int c = super.read();
            if (c == ZIP_LOCAL[ip]) {
                ip  ;
            }
            else ip = 0;
        }

        if (op < ZIP_LOCAL.length)
            return ZIP_LOCAL[op  ];
        else
            return super.read();
    }

    public int read(byte[] b, int off, int len) throws IOException {
        if (op == ZIP_LOCAL.length) return super.read(b, off, len);
        int l = 0;
        while (l < Math.min(len, ZIP_LOCAL.length)) {
            b[l  ] = (byte)read();
        }
        return l;
    }
}
 

Комментарии:

1. Большое вам спасибо, это мне очень помогло.

Ответ №2:

Самое приятное в ZIP-файлах — это их последовательная структура: каждая запись представляет собой независимую группу байтов, а в конце находится индекс центрального каталога, в котором перечислены все записи и их смещения в файле.

Плохо то, java.util.zip.* что классы игнорируют этот индекс и просто начинают чтение в файл, ожидая, что первой записью будет блок заголовка локального файла, что не относится к самораспаковывающимся ZIP-архивам (они начинаются с EXE-части).

Несколько лет назад я написал пользовательский ZIP-анализатор для извлечения отдельных ZIP-записей (LFH данные), который полагался на CDI, чтобы найти, где эти записи находятся в файле. Я только что проверил, и он действительно может перечислить записи самораспаковывающегося ZIP-архива без лишних слов и предоставить вам смещения — так что вы могли бы либо:

  1. используйте этот код, чтобы найти первый LFH после EXE-части и скопировать все после этого смещения в другое File , а затем передать это новое File в java.util.zip.ZipFile :

    Редактировать: просто пропустить EXE-часть, похоже, не работает, ZipFile она все равно не будет ее читать, и моя родная ZIP-программа жалуется, что новый ZIP-файл поврежден, и точное количество пропущенных байтов указано как «отсутствует» (так что он фактически считывает CDI). Я предполагаю, что некоторые заголовки нужно будет переписать, поэтому второй подход, приведенный ниже, выглядит более многообещающим — или

  2. используйте этот код для полного извлечения ZIP-файлов (он похож на java.util.zip ); для этого потребуется некоторое дополнительное подключение, поскольку изначально код не предназначался для замены ZIP-библиотеки, но имел очень специфический вариант использования (дифференциальное обновление ZIP-файлов по HTTP)

Код размещен в SourceForge (страница проекта, веб-сайт) и лицензирован под лицензией Apache License 2.0, поэтому коммерческое использование вполне допустимо — AFAIK есть коммерческая игра, использующая его в качестве средства обновления для своих игровых ресурсов.

Интересные части для получения смещений из ZIP-файла находятся в Indexer.parseZipFile which возвращает a LinkedHashMap<Resource, Long> (таким образом, первая запись карты имеет наименьшее смещение в файле). Вот код, который я использовал для перечисления записей самораспаковывающегося ZIP-архива (созданного с помощью WinZip SE creator с помощью Wine в Ubuntu из файла выпуска acra):

 public static void main(String[] args) throws Exception {
    File archive = new File("/home/phil/downloads", "acra-4.2.3.exe");
    Map<Resource, Long> resources = parseZipFile(archive);
    for (Entry<Resource, Long> resource : resources.entrySet()) {
        System.out.println(resource.getKey()   ": "   resource.getValue());
    }
}
 

Вероятно, вы можете удалить большую часть кода, за исключением Indexer класса и zip пакета, которые содержат все классы синтаксического анализа заголовков.

Комментарии:

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

Ответ №3:

В некоторых самораспаковывающихся ZIP-файлах есть поддельные маркеры заголовка локального файла. Я думаю, что лучше всего сканировать файл в обратном направлении, чтобы найти конец записи центрального каталога. Запись EOCD содержит смещение центрального каталога, а CD содержит смещение заголовка первого локального файла. Если вы начинаете чтение с первого байта заголовка локального файла ZipInputStream , все работает нормально.

Очевидно, что приведенный ниже код не является самым быстрым решением. Если вы собираетесь обрабатывать большие файлы, вам следует реализовать какую-то буферизацию или использовать файлы с отображением в памяти.

 import org.apache.commons.io.EndianUtils;
...

public class ZipHandler {
    private static final byte[] EOCD_MARKER = { 0x06, 0x05, 0x4b, 0x50 };

    public InputStream openExecutableZipFile(Path zipFilePath) throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(zipFilePath.toFile(), "r")) {
            long position = raf.length() - 1;
            int markerIndex = 0;
            byte[] buffer = new byte[4];
            while (position > EOCD_MARKER.length) {
                raf.seek(position);
                raf.read(buffer, 0 ,1);
                if (buffer[0] == EOCD_MARKER[markerIndex]) {
                    markerIndex  ;
                } else {
                    markerIndex = 0;
                }
                if (markerIndex == EOCD_MARKER.length) {
                    raf.skipBytes(15);
                    raf.read(buffer, 0, 4);
                    int centralDirectoryOffset = EndianUtils.readSwappedInteger(buffer, 0);
                    raf.seek(centralDirectoryOffset);
                    raf.skipBytes(42);
                    raf.read(buffer, 0, 4);
                    int localFileHeaderOffset = EndianUtils.readSwappedInteger(buffer, 0);
                    return new SkippingInputStream(Files.newInputStream(zipFilePath), localFileHeaderOffset);
                }
                position--;
            }
            throw new IOException("No EOCD marker found");
        }
    }
}

public class SkippingInputStream extends FilterInputStream {
    private int bytesToSkip;
    private int bytesAlreadySkipped;

    public SkippingInputStream(InputStream inputStream, int bytesToSkip) {
        super(inputStream);
        this.bytesToSkip = bytesToSkip;
        this.bytesAlreadySkipped = 0;
    }

    @Override
    public int read() throws IOException {
        while (bytesAlreadySkipped < bytesToSkip) {
            int c = super.read();
            if (c == -1) {
                return -1;
            }
            bytesAlreadySkipped  ;
        }
        return super.read();
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (bytesAlreadySkipped == bytesToSkip) {
            return super.read(b, off, len);
        }
        int count = 0;
        while (count < len) {
            int c = read();
            if (c == -1) {
                break;
            }
            b[count  ] = (byte) c;
        }
        return count;
    }
}
 

Ответ №4:

В этом случае лучше всего работает TrueZIP. (По крайней мере, в моем случае)

Самораспаковывающийся ZIP-файл имеет следующий формат code1 заголовок file1 file1 (в то время как обычный zip-файл имеет формат header1 file1)…Код рассказывает о том, как извлечь zip-файл

Хотя утилита извлечения Truezip жалуется на лишние байты и выдает исключение

Вот код

  private void Extract(String src, String dst, String incPath) {
    TFile srcFile = new TFile(src, incPath);
    TFile dstFile = new TFile(dst);
    try {
        TFile.cp_rp(srcFile, dstFile, TArchiveDetector.NULL);
        } 
    catch (IOException e) {
       //Handle Exception
        }
}
 

Вы можете вызвать этот метод, например, Extract (новая строка («C:2006Production.exe «), новая строка («c:») , «»);

Файл извлекается на диске c … вы можете выполнить свою собственную операцию с вашим файлом. Надеюсь, это поможет.

Спасибо.

Комментарии:

1. Почему вы ничего не делаете за исключением этого? И зачем возвращать логическое значение, если оно может быть только true?