如何从内存中的流式 zip 文件访问 zipEntry

How to access a zipEntry from a streamed zip file in memory

我目前正在实现一个电子阅读器库 (skyepub),它要求我实现一个方法来检查 zipEntry 是否存在。在他们的演示版本中,解决方案很简单:

public boolean isExists(String baseDirectory,String contentPath) {
    setupZipFile(baseDirectory,contentPath);
    if (this.isCustomFont(contentPath)) {
        String path = baseDirectory +"/"+ contentPath;
        File file = new File(path);
        return file.exists();
    }

    ZipEntry entry = this.getZipEntry(contentPath);
    if (entry==null) return false;
    else return true;       
}

// Entry name should start without / like META-INF/container.xml 

private ZipEntry getZipEntry(String contentPath) {

    if (zipFile==null) return null;

    String[] subDirs = contentPath.split(Pattern.quote(File.separator));

    String corePath = contentPath.replace(subDirs[1], "");

    corePath=corePath.replace("//", "");

    ZipEntry entry = zipFile.getEntry(corePath.replace(File.separatorChar, '/'));

    return entry;

}

如您所见,您可以使用 getZipEntry(contentPath);

在 O(1) 时间内访问有问题的 ZipEntry

但是,在我的例子中,我无法直接从文件系统读取 zip 文件(出于安全原因,它必须从内存中读取)。所以我的 ifExists 实现实际上是通过 zip 文件 一次一个条目,直到找到有问题的zipEntry,这里是相关部分:

try {
        final InputStream stream = dbUtil.getBookStream(bookEditionID);
        if( stream == null) return null;

        final ZipInputStream zip = new ZipInputStream(stream);

        ZipEntry entry;
        do {
            entry = zip.getNextEntry();
            if( entry == null) {
                zip.close();
                return null;
            }
        } while( !entry.getName().equals(zipEntryName));

    } catch( IOException e) {
        Log.e("demo", "Can't get content data for "+contentPath);
        return null;
    }

    return data;

所以如果数据存在,ifExistsreturns 为真,否则如果为空则为假。

问题

有没有办法在 O(1) 时间而不是 O(n) 时间内从整个 ZipInputStream 中找到有问题的 zip 条目?

相关

参见问题 和 回答。

如果存档的内容在内存中,那么它是可搜索的,您可以搜索中央目录并自己使用。 ZipFile 和 Apache Commons Compress 的同等功能现在都不能用于除 File 之外的任何其他东西,但其他开源库可能(不确定 zip4j)。

Apache Commons Compress'ZipFile 中搜索中央目录并解析它的代码应该很容易适应存档作为 byte[] 可用的情况。事实上,有一个尚未应用的补丁可以作为 COMPRESS-327.

的一部分提供帮助

zip 存档中的条目实际上无法在 O(1) 时间内加载。如果我们看一下 zip archive 的结构,它看起来像这样:

  [local file header 1]
  [encryption header 1]
  [file data 1]
  [data descriptor 1]
  ... 
  [local file header n]
  [encryption header n]
  [file data n]
  [data descriptor n]
  [archive decryption header] 
  [archive extra data record] 
  [central directory header 1]
  .
  [central directory header n]
  [zip64 end of central directory record]
  [zip64 end of central directory locator] 
  [end of central directory record]

基本上,压缩文件有一些 headers 加上一个 "central directory",其中包含有关文件的所有元数据(中央目录 headers)。查找条目的唯一有效方法是扫描中央目录 (more info):

...must not scan for entries from the top of the ZIP file, because only the central directory specifies where a file chunk starts

因为中央目录 headers 上没有索引,您只能在 O(n) 中获得一个条目,其中 n 是存档中的文件数。

更新: 不幸的是,我所知道的所有使用流而不是文件的 zip 库都使用本地文件 headers 并扫描包括内容在内的整个流。它们也不容易弯曲。避免扫描我发现的整个档案的唯一方法是自己调整一个库。

更新 2: 为了您的目的,我冒昧地修改了上述 zip4j 库。假设您在字节数组中读取了 zip 文件,并且添加了对 zip4j 版本 1.3.2 的依赖,您可以像这样使用 MemoryHeaderReader and RandomByteStream

String myZipFile = "...";
byte[] bytes = readFile();
MemoryHeaderReader headerReader = new MemoryHeaderReader(RandomAccessStream.fromBytes(bytes));
ZipModel zipModel = headerReader.readAllHeaders();
FileHeader myFile = Zip4jUtil.getFileHeader(zipModel, myZipFile)
boolean fileIsPresent = myFile != null;

它在 O(entryCount) 中工作,无需读取整个档案,这应该相当快。我还没有对它进行彻底的测试,但它应该会让您了解如何根据您的目的调整 zip4j。

从技术上讲,搜索总是 O(n),其中 n 是 zip 文件中的条目数,因为您有通过中央目录或通过本地 headers.

进行线性搜索

您似乎暗示 zip 文件已完全加载到内存中。在这种情况下,最快的做法是在中央目录中搜索条目。如果找到它,该目录条目将指向本地 header.

如果您对同一个 zip 文件进行大量搜索,那么您可以在 O(n) 时间,然后使用它在大约 O(1) 时间内搜索给定名称。