使用 zip4j 查找 zip 中项目的压缩数据的开始

Finding start of compressed data for items in a zip with zip4j

我正在尝试使用 zip4j 为每个 zip 条目查找压缩数据的开头。用于返回本地 header 偏移量的很棒的库,Java 的 ZipFile 不会这样做。但是我想知道是否有比我在下面做的更可靠的方法来开始压缩数据?提前致谢。

offset = header.getOffsetLocalHeader();
offset += 30; //add fixed file header
offset += header.getFilenameLength(); // add filename field length
offset += header.getExtraFieldLength(); //add extra field length

//not quite the right number, sometimes have to add 4
//seems to be some header data that is outside the extra field value 
offset += 4;

编辑 这是一个示例 zip: https://alexa-public.s3.amazonaws.com/test.zip

下面的代码可以正确解压缩每个项目,但是 如果没有 +4 将无法工作。

        String path = "/Users/test/Desktop/zip test/test.zip";
        List<FileHeader> fileHeaders = new ZipFile(path).getFileHeaders();
        for (FileHeader header : fileHeaders) {
            long offset = 30 + header.getOffsetLocalHeader() + header.getFileNameLength() + header.getExtraFieldLength();
            //fudge factor!
            offset += 4;

            RandomAccessFile f = new RandomAccessFile(path, "r");
            byte[] buffer = new byte[(int) header.getCompressedSize()];
            f.seek(offset);

            f.read(buffer, 0, (int) header.getCompressedSize());
            f.close();

            Inflater inf = new Inflater(true);
            inf.setInput(buffer);
            byte[] inflatedContent = new byte[(int) header.getUncompressedSize()];
            inf.inflate(inflatedContent);
            inf.end();
            FileOutputStream fos = new FileOutputStream(new File("/Users/test/Desktop/" + header.getFileName()));
            fos.write(inflatedContent);
            fos.close();
        }

你必须在你的例子中添加 4 到偏移量的原因是因为这个条目的中央目录(=文件header)中的额外数据字段的大小与本地文件中的不同header,并且根据 zip 规范,在中央目录和本地 header 中具有不同的额外数据字段大小是完全合法的。事实上,我们所说的额外数据字段,Extended Timestamp extra field (signature 0x5455),有一个官方定义,两者之间的长度不同。

Extended Timestamp extra field (signature 0x5455)

Local-header version:

| Value         | Size          | Description                           |
| ------------- |---------------|---------------------------------------|
| 0x5455        | Short         | tag for this extra block type ("UT")  |
| TSize         | Short         | total data size for this block        |
| Flags         | Byte          | info bits                             |
| (ModTime)     | Long          | time of last modification (UTC/GMT)   |
| (AcTime)      | Long          | time of last access (UTC/GMT)         |
| (CrTime)      | Long          | time of original creation (UTC/GMT)   |


 Central-header version:

| Value         | Size          | Description                           |
| ------------- |---------------|---------------------------------------|
| 0x5455        | Short         | tag for this extra block type ("UT")  |
| TSize         | Short         | total data size for this block        |
| Flags         | Byte          | info bits                             |
| (ModTime)     | Long          | time of last modification (UTC/GMT)   |

在您附加的示例 zip 文件中,创建 zip 文件的工具为此额外字段添加了一个 4 字节的附加信息,与中央目录相比。

依靠从中央目录到数据开始的额外字段长度可能容易出错。一个更可靠的方法来实现你想要的是从本地读取额外的字段长度 header。我稍微修改了您的代码以考虑从本地 header 而不是从中央 header 到达数据开头的额外字段长度。

import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.util.RawIO;
import org.junit.Test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

public class ZipTest {

  private static final int OFFSET_TO_EXTRA_FIELD_LENGTH_SIZE = 28;

  private RawIO rawIO = new RawIO();

  @Test
  public void testExtractWithDataOffset() throws IOException, DataFormatException {
    String basePath = "/Users/slingala/Downloads/test/";
    String path = basePath + "test.zip";
    List<FileHeader> fileHeaders = new ZipFile(path).getFileHeaders();

    for (FileHeader header : fileHeaders) {
      RandomAccessFile f = new RandomAccessFile(path, "r");
      byte[] buffer = new byte[(int) header.getCompressedSize()];
      f.seek(OFFSET_TO_EXTRA_FIELD_LENGTH_SIZE);
      int extraFieldLength = rawIO.readShortLittleEndian(f);

      f.skipBytes(header.getFileNameLength() + extraFieldLength);

      f.read(buffer, 0, (int) header.getCompressedSize());
      f.close();

      Inflater inf = new Inflater(true);
      inf.setInput(buffer);
      byte[] inflatedContent = new byte[(int) header.getUncompressedSize()];
      inf.inflate(inflatedContent);
      inf.end();

      FileOutputStream fos = new FileOutputStream(new File(basePath + header.getFileName()));
      fos.write(inflatedContent);
      fos.close();
    }
  }
}


旁注,我想知道你为什么要自己读取数据,处理inflater并提取内容?使用 zip4j,您可以使用 ZipFile.extractAll() 提取所有内容,或者如果您愿意,也可以使用 ZipFile.getInputStream() 提取带有流的 zip 文件中的每个条目。一个骨架示例是:

ZipFile zipFile = new ZipFile("filename.zip");
FileHeader fileHeader = zipFile.getFileHeader("entry_name_in_zip.txt");
InputStream inputStream = zipFile.getInputStream(fileHeader);

获得输入流后,您可以读取内容并将其写入任何输出流。这样您就可以提取 zip 文件中的每个条目,而无需自己处理充气器。