问题
On my android device, I need to extract a file (an xapk, that is a plain zip archive as far as I know) that I get from a content uri. I'm creating the ZipInputStream using this line of code:
ZipInputStream zis = new ZipInputStream(getContentResolver().openInputStream(zipUri));
And then I try to read the first entry of the archive with:
ZipEntry entry = zis.getNextEntry()
The problem is that I get this exception:
java.util.zip.ZipException: only DEFLATED entries can have EXT descriptor
I'm 100% sure that there is no 0bytes files in the archive, and I can extract the same archive with other utilities (RAR, unzip etc) in my device.
If I use a ZipFile with an hard coded path (so no content uri involved), I can extract the same archive without problems, so the issue is related to ZipInputStream with an uri. On the other hand, I can't use a ZipFile here because it doesn't support content uris.
回答1:
Unfortunately the only answer is currently:
Don't process ZIP files in streaming mode like ZipInputStream
does. It seems like all currently available ZIP processing components like ZipInputStream
from the JRE and ZipArchiveInputStream
from Apache commons-compress can not handle such ZIP files.
There is a very good description of the problem on the apache commons-compress help page:
ZIP archives know a feature called the data descriptor which is a way to store an entry's length after the entry's data. This can only work reliably if the size information can be taken from the central directory or the data itself can signal it is complete, which is true for data that is compressed using the DEFLATED compression algorithm.
ZipFile has access to the central directory and can extract entries using the data descriptor reliably. The same is true for ZipArchiveInputStream as long as the entry is DEFLATED. For STORED entries ZipArchiveInputStream can try to read ahead until it finds the next entry, but this approach is not safe and has to be enabled by a constructor argument explicitly.
https://commons.apache.org/proper/commons-compress/zip.html
Solution
The only possibility to avoid this problem is to use ZipFile
, however the ZipFile implementation for the JRE requires a real file, therefore you may have to save to data to a temporary file.
Or if you use instead ZipFile from Apache commons-compress and you already have the ZIP file completely in-memory you can avoid saving it to a temporary file using a SeekableInMemoryByteChannel
instead.
EDIT: solution of using in-memory ZipFile of Apache:
ByteArrayOutputStream().use { byteArrayOutputStream ->
inputStream.copyTo(byteArrayOutputStream)
ZipFile(SeekableInMemoryByteChannel(byteArrayOutputStream.toByteArray())).use {
for (entry in it.entries) {
it.getInputStream(entry).copyTo(someOutputStream)
}
}
}
回答2:
Try using commons-compress
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.17</version>
</dependency>
Here is a snippet
public void unzip(File zipFile, File destDir) throws IOException, ArchiveException {
String destDirectory = destDir.getAbsolutePath();
try (ArchiveInputStream i = new ZipArchiveInputStream(new
FileInputStream(zipFile), "UTF-8", false, true)) {
ArchiveEntry entry = null;
while ((entry = i.getNextEntry()) != null) {
if (!i.canReadEntryData(entry)) {
System.out.println("Can't read entry: " + entry);
continue;
}
String name = destDirectory + File.separator + entry.getName();
File f = new File(name);
if (entry.isDirectory()) {
if (!f.isDirectory() && !f.mkdirs()) {
throw new IOException("failed to create directory " + f);
}
} else {
File parent = f.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
throw new IOException("failed to create directory " + parent);
}
try (OutputStream o = Files.newOutputStream(f.toPath())) {
IOUtils.copy(i, o);
}
}
}
}
}
来源:https://stackoverflow.com/questions/47208272/android-zipinputstream-only-deflated-entries-can-have-ext-descriptor