feat(zip): provide direct InputStream of ZIP entries (PR #2509)

* chore: provide direct InputStream of ZIP entries

* use limited stream, return bytes without using stream

---------

Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
Jan S.
2025-05-24 21:36:10 +02:00
committed by GitHub
parent 97d04edb01
commit b6f27d8a1a
8 changed files with 142 additions and 34 deletions
@@ -17,8 +17,8 @@ import jadx.zip.IZipEntry;
import jadx.zip.IZipParser;
import jadx.zip.ZipContent;
import jadx.zip.ZipReaderOptions;
import jadx.zip.io.LimitedInputStream;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.LimitedInputStream;
public class FallbackZipParser implements IZipParser {
private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class);
@@ -0,0 +1,46 @@
package jadx.zip.io;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class ByteBufferBackedInputStream extends InputStream {
private final ByteBuffer buf;
private int markedPosition = 0;
public ByteBufferBackedInputStream(ByteBuffer buf) {
this.buf = buf;
}
public int read() throws IOException {
if (!buf.hasRemaining()) {
return -1;
}
return buf.get() & 0xFF;
}
@SuppressWarnings("NullableProblems")
public int read(byte[] bytes, int off, int len) throws IOException {
if (!buf.hasRemaining()) {
return -1;
}
int readLen = Math.min(len, buf.remaining());
buf.get(bytes, off, readLen);
return readLen;
}
@Override
public boolean markSupported() {
return true;
}
@Override
public synchronized void mark(int unused) {
markedPosition = buf.position();
}
@Override
public synchronized void reset() {
buf.position(markedPosition);
}
}
@@ -1,4 +1,4 @@
package jadx.zip.security;
package jadx.zip.io;
import java.io.FilterInputStream;
import java.io.IOException;
@@ -8,6 +8,7 @@ public class LimitedInputStream extends FilterInputStream {
private final long maxSize;
private long currentPos;
private long markPos;
public LimitedInputStream(InputStream in, long maxSize) {
super(in);
@@ -50,7 +51,14 @@ public class LimitedInputStream extends FilterInputStream {
}
@Override
public boolean markSupported() {
return false;
public void mark(int readLimit) {
super.mark(readLimit);
markPos = currentPos;
}
@Override
public void reset() throws IOException {
super.reset();
currentPos = markPos;
}
}
@@ -1,6 +1,5 @@
package jadx.zip.parser;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -23,6 +22,8 @@ import jadx.zip.ZipContent;
import jadx.zip.ZipReaderFlags;
import jadx.zip.ZipReaderOptions;
import jadx.zip.fallback.FallbackZipParser;
import jadx.zip.io.ByteBufferBackedInputStream;
import jadx.zip.io.LimitedInputStream;
import jadx.zip.security.IJadxZipSecurity;
/**
@@ -46,6 +47,7 @@ public final class JadxZipParser implements IZipParser {
private final IJadxZipSecurity zipSecurity;
private final Set<ZipReaderFlags> flags;
private final boolean verify;
private final boolean useLimitedDataStream;
private RandomAccessFile file;
private FileChannel fileChannel;
@@ -61,6 +63,7 @@ public final class JadxZipParser implements IZipParser {
this.zipSecurity = options.getZipSecurity();
this.flags = options.getFlags();
this.verify = options.getFlags().contains(ZipReaderFlags.REPORT_TAMPERING);
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
}
@Override
@@ -287,47 +290,74 @@ public final class JadxZipParser implements IZipParser {
}
}
InputStream getInputStream(JadxZipEntry entry) {
return new ByteArrayInputStream(getBytes(entry));
synchronized InputStream getInputStream(JadxZipEntry entry) {
if (verify) {
verifyEntry(entry);
}
InputStream stream;
if (entry.getCompressMethod() == 8) {
try {
stream = ZipDeflate.decompressEntryToStream(byteBuffer, entry);
} catch (Exception e) {
entryParseFailed(entry, e);
return useFallbackParser(entry).getInputStream();
}
} else {
// treat any other compression methods values as UNCOMPRESSED
stream = bufferToStream(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize());
}
if (useLimitedDataStream) {
return new LimitedInputStream(stream, entry.getUncompressedSize());
}
return stream;
}
synchronized byte[] getBytes(JadxZipEntry entry) {
int compressMethod = entry.getCompressMethod();
if (verify) {
if (compressMethod == 0) {
if (entry.getCompressedSize() != entry.getUncompressedSize()) {
LOG.warn("Not equal sizes for STORE method: compressed: {}, uncompressed: {}, entry: {}",
entry.getCompressedSize(), entry.getUncompressedSize(), entry);
}
} else if (compressMethod != 8) {
LOG.warn("Unknown compress method: {} in entry: {}", compressMethod, entry);
}
verifyEntry(entry);
}
if (compressMethod == 8) {
if (entry.getCompressMethod() == 8) {
try {
return ZipDeflate.decompressEntryToBytes(byteBuffer, entry);
} catch (Exception e) {
if (isEncrypted(entry)) {
throw new RuntimeException("Entry is encrypted, failed to decompress: " + entry, e);
}
if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new RuntimeException("Failed to decompress zip entry: " + entry + ", error: " + e.getMessage(), e);
}
LOG.warn("Entry '{}' parse failed, switching to fallback parser", entry, e);
return useFallbackParser(entry);
entryParseFailed(entry, e);
return useFallbackParser(entry).getBytes();
}
}
// treat any other compression methods values as UNCOMPRESSED
return bufferToBytes(entry.getDataStart(), (int) entry.getUncompressedSize());
return bufferToBytes(byteBuffer, entry.getDataStart(), (int) entry.getUncompressedSize());
}
private static void verifyEntry(JadxZipEntry entry) {
int compressMethod = entry.getCompressMethod();
if (compressMethod == 0) {
if (entry.getCompressedSize() != entry.getUncompressedSize()) {
LOG.warn("Not equal sizes for STORE method: compressed: {}, uncompressed: {}, entry: {}",
entry.getCompressedSize(), entry.getUncompressedSize(), entry);
}
} else if (compressMethod != 8) {
LOG.warn("Unknown compress method: {} in entry: {}", compressMethod, entry);
}
}
private void entryParseFailed(JadxZipEntry entry, Exception e) {
if (isEncrypted(entry)) {
throw new RuntimeException("Entry is encrypted, failed to decompress: " + entry, e);
}
if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
throw new RuntimeException("Failed to decompress zip entry: " + entry + ", error: " + e.getMessage(), e);
}
LOG.warn("Entry '{}' parse failed, switching to fallback parser", entry, e);
}
@SuppressWarnings("resource")
private byte[] useFallbackParser(JadxZipEntry entry) {
private IZipEntry useFallbackParser(JadxZipEntry entry) {
LOG.debug("useFallbackParser used for {}", entry);
IZipEntry zipEntry = initFallbackParser().searchEntry(entry.getName());
if (zipEntry == null) {
throw new RuntimeException("Fallback parser can't find entry: " + entry);
}
return zipEntry.getBytes();
return zipEntry;
}
@SuppressWarnings("resource")
@@ -353,14 +383,20 @@ public final class JadxZipParser implements IZipParser {
return readU2(buf);
}
byte[] bufferToBytes(int start, int size) {
ByteBuffer buf = byteBuffer;
static byte[] bufferToBytes(ByteBuffer buf, int start, int size) {
byte[] data = new byte[size];
buf.position(start);
buf.get(data);
return data;
}
static InputStream bufferToStream(ByteBuffer buf, int start, int size) {
buf.position(start);
ByteBuffer streamBuf = buf.slice();
streamBuf.limit(size);
return new ByteBufferBackedInputStream(streamBuf);
}
private static int readU2(ByteBuffer buf) {
return buf.getShort() & 0xFFFF;
}
@@ -1,10 +1,15 @@
package jadx.zip.parser;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import static jadx.zip.parser.JadxZipParser.bufferToStream;
final class ZipDeflate {
private static final int BUFFER_SIZE = 4096;
static byte[] decompressEntryToBytes(ByteBuffer buf, JadxZipEntry entry) throws DataFormatException {
buf.position(entry.getDataStart());
@@ -14,11 +19,17 @@ final class ZipDeflate {
Inflater inflater = new Inflater(true);
inflater.setInput(entryBuf);
int written = inflater.inflate(out);
inflater.end();
if (written != out.length) {
throw new DataFormatException("Unexpected size of decompressed entry: " + entry
+ ", got: " + written + ", expected: " + out.length);
}
inflater.end();
return out;
}
static InputStream decompressEntryToStream(ByteBuffer buf, JadxZipEntry entry) {
InputStream stream = bufferToStream(buf, entry.getDataStart(), (int) entry.getCompressedSize());
Inflater inflater = new Inflater(true);
return new InflaterInputStream(stream, inflater, BUFFER_SIZE);
}
}
@@ -27,6 +27,8 @@ public class JadxZipSecurity implements IJadxZipSecurity {
private int maxEntriesCount = 100_000;
private boolean useLimitedDataStream = true;
@Override
public boolean isValidEntry(IZipEntry entry) {
return isValidEntryName(entry.getName()) && !isZipBomb(entry);
@@ -34,7 +36,7 @@ public class JadxZipSecurity implements IJadxZipSecurity {
@Override
public boolean useLimitedDataStream() {
return false;
return useLimitedDataStream;
}
@Override
@@ -115,6 +117,10 @@ public class JadxZipSecurity implements IJadxZipSecurity {
this.zipBombMinUncompressedSize = zipBombMinUncompressedSize;
}
public void setUseLimitedDataStream(boolean useLimitedDataStream) {
this.useLimitedDataStream = useLimitedDataStream;
}
private static File getCWD() {
try {
return new File(".").getCanonicalFile();
@@ -16,10 +16,10 @@ import jadx.api.plugins.JadxPluginContext;
import jadx.core.utils.Utils;
import jadx.zip.IZipEntry;
import jadx.zip.ZipReader;
import jadx.zip.io.LimitedInputStream;
import jadx.zip.security.DisabledZipSecurity;
import jadx.zip.security.IJadxZipSecurity;
import jadx.zip.security.JadxZipSecurity;
import jadx.zip.security.LimitedInputStream;
/**
* Deprecated, migrate to {@link ZipReader}. <br>
@@ -1,5 +1,6 @@
package jadx.core.xmlgen;
import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
@@ -21,7 +22,7 @@ public class ParserStream {
private long markPos = 0;
public ParserStream(@NotNull InputStream inputStream) {
this.input = inputStream;
this.input = inputStream.markSupported() ? inputStream : new BufferedInputStream(inputStream);
}
public long getPos() {