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:
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user