Как я могу прочитать самораспаковывающийся (exe) zip-файл Winzip на Java?
Существует ли существующий метод, или мне нужно будет вручную проанализировать и пропустить exe-блок перед передачей данных в ZipInputStream?
4 ответа
После просмотра формата файла EXE и формата файла 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;
}
}
Приятной особенностью ZIP-файлов является их последовательная структура: каждая запись представляет собой независимую группу байтов, а в конце - индекс центрального каталога, в котором перечислены все записи и их смещения в файле.
Плохо то, что java.util.zip.*
классы игнорируют этот индекс и просто начинают чтение в файл и ожидают, что первая запись будет блоком локального заголовка файла, чего нельзя сказать о самораспаковывающихся ZIP-архивах (они начинаются с части EXE).
Несколько лет назад я написал собственный анализатор ZIP для извлечения отдельных записей ZIP (данные LFH +), которые полагались на CDI, чтобы найти, где эти записи находятся в файле. Я только что проверил, и он может фактически перечислить записи самораспаковывающегося ZIP-архива без лишних слов и дать вам смещения - так что вы можете:
используйте этот код, чтобы найти первый LFH после части EXE, и скопируйте все после этого смещения в другойFile
, затем накорми это новоеFile
вjava.util.zip.ZipFile
:Изменить: просто пропустить часть EXE, кажется, не работает,
ZipFile
все еще не читает его, и моя родная программа ZIP жалуется, что новый файл ZIP поврежден, и именно количество пропущенных байтов указано как "пропущенное" (поэтому оно фактически читает CDI). Я предполагаю, что некоторые заголовки нужно будет переписать, поэтому второй подход, приведенный ниже, выглядит более многообещающим - или- используйте этот код для полного извлечения ZIP (он похож на
java.util.zip
); это потребовало бы некоторого дополнительного подключения, потому что код изначально не предназначался для замены ZIP-библиотеки, но имел очень специфический вариант использования (дифференциальное обновление ZIP-файлов по HTTP)
Код размещен на SourceForge ( страница проекта, веб-сайт) и распространяется по лицензии Apache License 2.0, поэтому его можно использовать в коммерческих целях - AFAIK, есть коммерческая игра, использующая его в качестве средства обновления своих игровых ресурсов.
Интересные части, чтобы получить смещения из файла ZIP находятся в Indexer.parseZipFile
который возвращает LinkedHashMap<Resource, Long>
(поэтому первая запись карты имеет самое низкое смещение в файле). Вот код, который я использовал для перечисления записей самораспаковывающегося ZIP-архива (созданного с помощью создателя WinZIP SE с 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
пакет, который содержит все классы разбора заголовка.
В некоторых самораспаковывающихся ZIP-файлах есть фальшивые маркеры локальных заголовков файлов. Я думаю, что лучше сканировать файл в обратном направлении, чтобы найти запись End Of Central Directory. Запись 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;
}
}
TrueZip работает лучше всего в этом случае. (По крайней мере в моем случае)
Самораспаковывающийся zip-файл имеет следующий формат code1 header1 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(new String("C:\2006Production.exe"), new String("c:\"), "");
Файл извлекается на диске c... вы можете выполнить свою операцию над своим файлом. Надеюсь, это поможет.
Благодарю.