You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2025/08/14 20:29:19 UTC

(commons-compress) branch master updated: org.apache.commons.compress.archivers.zip.ZipArchiveInputStream now throws an MemoryLimitException instead of ArchiveException, or OutOfMemoryError when running with low memory settings set on the command line

This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git


The following commit(s) were added to refs/heads/master by this push:
     new 703751f06 org.apache.commons.compress.archivers.zip.ZipArchiveInputStream now throws an MemoryLimitException instead of ArchiveException, or OutOfMemoryError when running with low memory settings set on the command line
703751f06 is described below

commit 703751f069736f4cd1b98ba2d3acf1d1c0d29b27
Author: Gary D. Gregory <ga...@gmail.com>
AuthorDate: Thu Aug 14 16:29:13 2025 -0400

    org.apache.commons.compress.archivers.zip.ZipArchiveInputStream now
    throws an MemoryLimitException instead of ArchiveException, or
    OutOfMemoryError when running with low memory settings set on the
    command line
---
 src/changes/changes.xml                            |   1 +
 .../archivers/zip/ZipArchiveInputStream.java       | 102 +++------------------
 .../zip/ZipArchiveInputStreamMalformedTest.java    |  77 ++++++++++++++++
 3 files changed, 91 insertions(+), 89 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 590af9bcb..80034556d 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -68,6 +68,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Tyler Nighswander, Gary Gregory">org.apache.commons.compress.archivers.zip.ZipFile.getContentBeforeFirstLocalFileHeader() now throws an ArchiveException instead of IllegalArgumentException.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">org.apache.commons.compress.archivers.zip.ExplodingInputStream now throws ArchiveException instead of ArithmeticException.</action>
       <action type="fix" dev="ggregory" due-to="Tyler Nighswander, Gary Gregory">org.apache.commons.compress.archivers.zip.ZipArchiveInputStream.read() now throws an IOException instead of java.lang.ArrayIndexOutOfBoundsException.</action>
+      <action type="fix" dev="ggregory" due-to="Stanislav Fort, Gary Gregory">org.apache.commons.compress.archivers.zip.ZipArchiveInputStream now throws an MemoryLimitException instead of ArchiveException, or OutOfMemoryError when running with low memory settings set on the command line.</action>
       <!-- FIX tar -->      
       <action type="fix" dev="pkarwasz" due-to="Tyler Nighswander, Piotr P. Karwasz, Gary Gregory">>Uniform handling of special tar records in TarFile and TarArchiveInputStream.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory, Stanislav Fort">TarArchiveOutputStream now throws a IllegalArgumentException instead of an OutOfMemoryError.</action>
diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
index 3108e0b04..38f985116 100644
--- a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
+++ b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStream.java
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 package org.apache.commons.compress.archivers.zip;
 
 import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD;
@@ -40,6 +41,7 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipException;
 
+import org.apache.commons.compress.MemoryLimitException;
 import org.apache.commons.compress.archivers.ArchiveEntry;
 import org.apache.commons.compress.archivers.ArchiveException;
 import org.apache.commons.compress.archivers.ArchiveInputStream;
@@ -79,13 +81,12 @@ public class ZipArchiveInputStream extends ArchiveInputStream<ZipArchiveEntry> i
      * Input stream adapted from commons-io.
      */
     private final class BoundCountInputStream extends BoundedInputStream {
-
         // TODO Consider how to do this from a final class, an IO class, or basically without the current side-effect implementation.
 
         /**
          * Creates a new {@code BoundedInputStream} that wraps the given input stream and limits it to a certain size.
          *
-         * @param in   The wrapped input stream
+         * @param in  The wrapped input stream
          * @param max The maximum number of bytes to return
          */
         BoundCountInputStream(final InputStream in, final long max) {
@@ -127,7 +128,6 @@ private int readCount(final int bytesRead) {
             }
             return bytesRead;
         }
-
     }
 
     /**
@@ -139,22 +139,18 @@ private static final class CurrentEntry {
          * Current ZIP entry.
          */
         private final ZipArchiveEntry entry = new ZipArchiveEntry();
-
         /**
          * Does the entry use a data descriptor?
          */
         private boolean hasDataDescriptor;
-
         /**
          * Does the entry have a ZIP64 extended information extra field.
          */
         private boolean usesZip64;
-
         /**
          * Number of bytes of entry content read by the client if the entry is STORED.
          */
         private long bytesRead;
-
         /**
          * Number of bytes of entry content read from the stream.
          * <p>
@@ -163,12 +159,10 @@ private static final class CurrentEntry {
          * </p>
          */
         private long bytesReadFromStream;
-
         /**
          * The checksum calculated as the current entry is read.
          */
         private final CRC32 crc = new CRC32();
-
         /**
          * The input stream decompressing the data for shrunk and imploded entries.
          */
@@ -187,42 +181,33 @@ private <T extends InputStream> T checkInputStream() throws ZipException {
      * Maximum size of data in the first local file header.
      */
     public static final int PREAMBLE_GARBAGE_MAX_SIZE = 4096;
-
     private static final int LFH_LEN = 30;
-
     /*
      * local file header signature WORD version needed to extract SHORT general purpose bit flag SHORT compression method SHORT last mod file time SHORT last
      * mod file date SHORT CRC-32 WORD compressed size WORD uncompressed size WORD file name length SHORT extra field length SHORT
      */
     private static final int CFH_LEN = 46;
-
     /*
      * central file header signature WORD version made by SHORT version needed to extract SHORT general purpose bit flag SHORT compression method SHORT last mod
      * file time SHORT last mod file date SHORT CRC-32 WORD compressed size WORD uncompressed size WORD file name length SHORT extra field length SHORT file
      * comment length SHORT disk number start SHORT internal file attributes SHORT external file attributes WORD relative offset of local header WORD
      */
     private static final long TWO_EXP_32 = ZIP64_MAGIC + 1;
-
     private static final String USE_ZIPFILE_INSTEAD_OF_STREAM_DISCLAIMER = " while reading a stored entry using data descriptor. Either the archive is broken"
             + " or it cannot be read using ZipArchiveInputStream and you must use ZipFile."
             + " A common cause for this is a ZIP archive containing a ZIP archive."
             + " See https://commons.apache.org/proper/commons-compress/zip.html#ZipArchiveInputStream_vs_ZipFile";
-
     private static final byte[] LFH = ZipLong.LFH_SIG.getBytes();
-
     private static final byte[] CFH = ZipLong.CFH_SIG.getBytes();
-
     private static final byte[] DD = ZipLong.DD_SIG.getBytes();
-
     private static final byte[] APK_SIGNING_BLOCK_MAGIC = { 'A', 'P', 'K', ' ', 'S', 'i', 'g', ' ', 'B', 'l', 'o', 'c', 'k', ' ', '4', '2', };
-
     private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE);
 
     /**
      * Checks if the signature matches what is expected for a ZIP file. Does not currently handle self-extracting ZIPs which may have arbitrary leading content.
      *
      * @param buffer the bytes to check
-     * @param length    the number of bytes to check
+     * @param length the number of bytes to check
      * @return true, if this stream is a ZIP archive stream, false otherwise
      */
     public static boolean matches(final byte[] buffer, final int length) {
@@ -237,30 +222,22 @@ public static boolean matches(final byte[] buffer, final int length) {
 
     /** The ZIP encoding to use for file names and the file comment. */
     private final ZipEncoding zipEncoding;
-
     /** Whether to look for and use Unicode extra fields. */
     private final boolean useUnicodeExtraFields;
-
     /** Inflater used for all deflated entries. */
     private final Inflater inf = new Inflater(true);
-
     /** Buffer used to read from the wrapped stream. */
     private final ByteBuffer buf = ByteBuffer.allocate(ZipArchiveOutputStream.BUFFER_SIZE);
-
     /** The entry that is currently being read. */
     private CurrentEntry current;
-
     /** Whether the stream has been closed. */
     private boolean closed;
-
     /** Whether the stream has reached the central directory - and thus found all entries. */
     private boolean hitCentralDirectory;
-
     /**
      * When reading a stored entry that uses the data descriptor this stream has to read the full entry and caches it. This is the cache.
      */
     private ByteArrayInputStream lastStoredEntry;
-
     /**
      * Whether the stream will try to read STORED entries that use a data descriptor. Setting it to true means we will not stop reading an entry with the
      * compressed size, instead we will stop reading an entry when a data descriptor is met (by finding the Data Descriptor Signature). This will completely
@@ -271,26 +248,17 @@ public static boolean matches(final byte[] buffer, final int length) {
      * </p>
      */
     private final boolean allowStoredEntriesWithDataDescriptor;
-
     /** Count decompressed bytes for current entry */
     private long uncompressedCount;
-
     /** Whether the stream will try to skip the ZIP split signature(08074B50) at the beginning **/
     private final boolean skipSplitSig;
-
     /** Cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection). */
     private final byte[] lfhBuf = new byte[LFH_LEN];
-
     private final byte[] skipBuf = new byte[1024];
-
     private final byte[] shortBuf = new byte[SHORT];
-
     private final byte[] wordBuf = new byte[WORD];
-
     private final byte[] twoDwordBuf = new byte[2 * DWORD];
-
     private int entriesRead;
-
     /**
      * The factory for extra fields or null.
      */
@@ -372,7 +340,6 @@ public ZipArchiveInputStream(final InputStream inputStream, final String encodin
      * </p>
      */
     private boolean bufferContainsSignature(final ByteArrayOutputStream bos, final int offset, final int lastRead, final int expectedDDLen) throws IOException {
-
         boolean done = false;
         for (int i = 0; !done && i < offset + lastRead - 4; i++) {
             if (buf.array()[i] == LFH[0] && buf.array()[i + 1] == LFH[1]) {
@@ -407,9 +374,11 @@ private boolean bufferContainsSignature(final ByteArrayOutputStream bos, final i
      * Data descriptor plus incomplete signature (3 bytes in the worst case) can be 20 bytes max.
      * </p>
      */
-    private int cacheBytesRead(final ByteArrayOutputStream bos, int offset, final int lastRead, final int expectedDDLen) {
+    private int cacheBytesRead(final ByteArrayOutputStream bos, int offset, final int lastRead, final int expectedDDLen) throws MemoryLimitException {
         final int cacheable = offset + lastRead - expectedDDLen - 3;
         if (cacheable > 0) {
+            final int request = (bos.size() + cacheable) * 2;
+            MemoryLimitException.checkBytes(request + 2 * 1024 * 1024, Runtime.getRuntime().maxMemory()); // 2 MB headroom
             bos.write(buf.array(), 0, cacheable);
             System.arraycopy(buf.array(), cacheable, buf.array(), 0, expectedDDLen + 3);
             offset = expectedDDLen + 3;
@@ -469,7 +438,6 @@ private void closeEntry() throws IOException {
         if (current == null) {
             return;
         }
-
         // Ensure all entry bytes are read
         if (currentEntryHasOutstandingBytes()) {
             drainCurrentEntryData();
@@ -478,29 +446,23 @@ private void closeEntry() throws IOException {
             if (skip(Long.MAX_VALUE) < 0) {
                 throw new IllegalStateException("Can't read the remainder of the stream");
             }
-
             final long inB = current.entry.getMethod() == ZipArchiveOutputStream.DEFLATED ? getBytesInflated() : current.bytesRead;
-
             // this is at most a single read() operation and can't
             // exceed the range of int
             final int diff = (int) (current.bytesReadFromStream - inB);
-
             // Pushback any required bytes
             if (diff > 0) {
                 pushback(buf.array(), buf.limit() - diff, diff);
                 current.bytesReadFromStream -= diff;
             }
-
             // Drain remainder of entry if not all data bytes were required
             if (currentEntryHasOutstandingBytes()) {
                 drainCurrentEntryData();
             }
         }
-
         if (lastStoredEntry == null && current.hasDataDescriptor) {
             readDataDescriptor();
         }
-
         inf.reset();
         buf.clear().flip();
         current = null;
@@ -655,7 +617,6 @@ public ZipArchiveEntry getNextEntry() throws IOException {
     @Deprecated
     public ZipArchiveEntry getNextZipEntry() throws IOException {
         uncompressedCount = 0;
-
         boolean firstEntry = true;
         if (closed || hitCentralDirectory) {
             return null;
@@ -664,7 +625,6 @@ public ZipArchiveEntry getNextZipEntry() throws IOException {
             closeEntry();
             firstEntry = false;
         }
-
         final long currentHeaderOffset = getBytesRead();
         try {
             if (firstEntry) {
@@ -683,7 +643,6 @@ public ZipArchiveEntry getNextZipEntry() throws IOException {
         } catch (final EOFException e) { // NOSONAR
             return null;
         }
-
         final ZipLong sig = new ZipLong(lfhBuf);
         if (!sig.equals(ZipLong.LFH_SIG)) {
             if (sig.equals(ZipLong.CFH_SIG) || sig.equals(ZipLong.AED_SIG) || isApkSigningBlock(lfhBuf)) {
@@ -700,66 +659,50 @@ public ZipArchiveEntry getNextZipEntry() throws IOException {
         final int versionMadeBy = ZipShort.getValue(lfhBuf, off);
         off += SHORT;
         current.entry.setPlatform(ZipFile.toPlatform(versionMadeBy));
-
         final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(lfhBuf, off);
         final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames();
         final ZipEncoding entryEncoding = hasUTF8Flag ? ZipEncodingHelper.ZIP_ENCODING_UTF_8 : zipEncoding;
         current.hasDataDescriptor = gpFlag.usesDataDescriptor();
         current.entry.setGeneralPurposeBit(gpFlag);
-
         off += SHORT;
-
         current.entry.setMethod(ZipShort.getValue(lfhBuf, off));
         off += SHORT;
-
         final long time = ZipUtil.dosToJavaTime(ZipLong.getValue(lfhBuf, off));
         current.entry.setTime(time);
         off += WORD;
-
         ZipLong size = null;
         ZipLong cSize = null;
         if (!current.hasDataDescriptor) {
             current.entry.setCrc(ZipLong.getValue(lfhBuf, off));
             off += WORD;
-
             cSize = new ZipLong(lfhBuf, off);
             off += WORD;
-
             size = new ZipLong(lfhBuf, off);
             off += WORD;
         } else {
             off += 3 * WORD;
         }
-
         final int fileNameLen = ZipShort.getValue(lfhBuf, off);
-
         off += SHORT;
-
         final int extraLen = ZipShort.getValue(lfhBuf, off);
-
         final byte[] fileName = readRange(fileNameLen);
         current.entry.setName(entryEncoding.decode(fileName), fileName);
         if (hasUTF8Flag) {
             current.entry.setNameSource(ZipArchiveEntry.NameSource.NAME_WITH_EFS_FLAG);
         }
-
         final byte[] extraData = readRange(extraLen);
         try {
             current.entry.setExtra(extraData);
         } catch (final RuntimeException ex) {
             throw ZipUtil.newZipException("Invalid extra data in entry " + current.entry.getName(), ex);
         }
-
         if (!hasUTF8Flag && useUnicodeExtraFields) {
             ZipUtil.setNameAndCommentFromExtraFields(current.entry, fileName, null);
         }
-
         processZip64Extra(size, cSize);
-
         current.entry.setLocalHeaderOffset(currentHeaderOffset);
         current.entry.setDataOffset(getBytesRead());
         current.entry.setStreamContiguous(true);
-
         final ZipMethod m = ZipMethod.getMethodByCode(current.entry.getMethod());
         if (current.entry.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN) {
             if (ZipUtil.canHandleEntryData(current.entry) && m != ZipMethod.STORED && m != ZipMethod.DEFLATED) {
@@ -796,7 +739,6 @@ public ZipArchiveEntry getNextZipEntry() throws IOException {
         } else if (m == ZipMethod.ENHANCED_DEFLATED) {
             current.inputStream = new Deflate64CompressorInputStream(in);
         }
-
         entriesRead++;
         return current.entry;
     }
@@ -829,7 +771,6 @@ private boolean isApkSigningBlock(final byte[] suspectLocalFileHeader) throws IO
         // also subtract 16 bytes in order to position us at the magic string
         BigInteger toSkip = len.add(BigInteger.valueOf(DWORD - suspectLocalFileHeader.length - (long) APK_SIGNING_BLOCK_MAGIC.length));
         final byte[] magic = new byte[APK_SIGNING_BLOCK_MAGIC.length];
-
         try {
             if (toSkip.signum() < 0) {
                 // suspectLocalFileHeader contains the start of suspect magic string
@@ -961,13 +902,11 @@ private void readDataDescriptor() throws IOException {
             val = new ZipLong(wordBuf);
         }
         current.entry.setCrc(val.getValue());
-
         // if there is a ZIP64 extra field, sizes are eight bytes
         // each, otherwise four bytes each. Unfortunately some
         // implementations - namely Java7 - use eight bytes without
         // using a ZIP64 extra field -
         // https://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7073588
-
         // just read 16 bytes and check whether bytes nine to twelve
         // look like one of the signatures of what could follow a data
         // descriptor (ignoring archive decryption headers for now).
@@ -1028,12 +967,10 @@ private boolean readFirstLocalFileHeader() throws IOException {
         final byte[] header = new byte[Math.min(LFH_LEN, ZipFile.MIN_EOCD_SIZE)];
         readFully(header);
         try {
-            READ_LOOP: for (int i = 0; ; ) {
+            READ_LOOP: for (int i = 0;;) {
                 for (int j = 0; i <= PREAMBLE_GARBAGE_MAX_SIZE - 4 && j <= header.length - 4; ++j, ++i) {
                     final ZipLong sig = new ZipLong(header, j);
-                    if (sig.equals(ZipLong.LFH_SIG) ||
-                        sig.equals(ZipLong.SINGLE_SEGMENT_SPLIT_MARKER) ||
-                        sig.equals(ZipLong.DD_SIG)) {
+                    if (sig.equals(ZipLong.LFH_SIG) || sig.equals(ZipLong.SINGLE_SEGMENT_SPLIT_MARKER) || sig.equals(ZipLong.DD_SIG)) {
                         // regular archive containing at least one entry:
                         System.arraycopy(header, j, header, 0, header.length - j);
                         readFully(header, header.length - j);
@@ -1057,11 +994,9 @@ private boolean readFirstLocalFileHeader() throws IOException {
             throw new ZipException("Cannot find zip signature within the file");
         }
         final ZipLong sig = new ZipLong(lfhBuf);
-
         if (!skipSplitSig && sig.equals(ZipLong.DD_SIG)) {
             throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.SPLITTING);
         }
-
         // the split ZIP signature(08074B50) should only be skipped when the skipSplitSig is set
         if (sig.equals(ZipLong.SINGLE_SEGMENT_SPLIT_MARKER) || sig.equals(ZipLong.DD_SIG)) {
             // Just skip over the marker.
@@ -1108,7 +1043,6 @@ private void readFully(final byte[] b, final int off) throws IOException {
             throw new EOFException();
         }
     }
-
     // End of Central Directory Record
     // end of central dir signature WORD
     // number of this disk SHORT
@@ -1152,19 +1086,16 @@ private byte[] readRange(final int len) throws IOException {
      * Implements read for STORED entries.
      */
     private int readStored(final byte[] buffer, final int offset, final int length) throws IOException {
-
         if (current.hasDataDescriptor) {
             if (lastStoredEntry == null) {
                 readStoredEntry();
             }
             return lastStoredEntry.read(buffer, offset, length);
         }
-
         final long csize = current.entry.getSize();
         if (current.bytesRead >= csize) {
             return -1;
         }
-
         if (buf.position() >= buf.limit()) {
             buf.position(0);
             final int l = in.read(buf.array());
@@ -1173,11 +1104,9 @@ private int readStored(final byte[] buffer, final int offset, final int length)
                 throw new ArchiveException("Truncated ZIP file");
             }
             buf.limit(l);
-
             count(l);
             current.bytesReadFromStream += l;
         }
-
         int toRead = Math.min(buf.remaining(), length);
         if (csize - current.bytesRead < toRead) {
             // if it is smaller than toRead then it fits into an int
@@ -1206,10 +1135,8 @@ private void readStoredEntry() throws IOException {
         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
         int off = 0;
         boolean done = false;
-
         // length of DD without signature
         final int ddLen = current.usesZip64 ? WORD + 2 * DWORD : 3 * WORD;
-
         while (!done) {
             final int r = in.read(buf.array(), off, ZipArchiveOutputStream.BUFFER_SIZE - off);
             if (r <= 0) {
@@ -1222,7 +1149,6 @@ private void readStoredEntry() throws IOException {
                 off += r;
                 continue;
             }
-
             done = bufferContainsSignature(bos, off, r, ddLen);
             if (!done) {
                 off = cacheBytesRead(bos, off, r, ddLen);
@@ -1264,6 +1190,7 @@ private void realSkip(final long value) throws IOException {
      * Currently unused.
      *
      * Sets the custom extra fields factory.
+     *
      * @param extraFieldSupport the lookup function based on extra field header id.
      * @return the archive.
      */
@@ -1332,11 +1259,9 @@ private void skipRemainderOfArchive() throws IOException {
      */
     private boolean supportsCompressedSizeFor(final ZipArchiveEntry entry) {
         final int method = entry.getMethod();
-        return entry.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN || method == ZipEntry.DEFLATED
-                || method == ZipMethod.ENHANCED_DEFLATED.getCode()
+        return entry.getCompressedSize() != ArchiveEntry.SIZE_UNKNOWN || method == ZipEntry.DEFLATED || method == ZipMethod.ENHANCED_DEFLATED.getCode()
                 || entry.getGeneralPurposeBit().usesDataDescriptor() && allowStoredEntriesWithDataDescriptor && method == ZipEntry.STORED
-                || ZipMethod.isZstd(method)
-                || method == ZipMethod.XZ.getCode();
+                || ZipMethod.isZstd(method) || method == ZipMethod.XZ.getCode();
     }
 
     /**
@@ -1348,8 +1273,7 @@ private boolean supportsCompressedSizeFor(final ZipArchiveEntry entry) {
     private boolean supportsDataDescriptorFor(final ZipArchiveEntry entry) {
         final int method = entry.getMethod();
         return !entry.getGeneralPurposeBit().usesDataDescriptor() || allowStoredEntriesWithDataDescriptor && method == ZipEntry.STORED
-                || method == ZipEntry.DEFLATED || method == ZipMethod.ENHANCED_DEFLATED.getCode()
-                || ZipMethod.isZstd(method)
+                || method == ZipEntry.DEFLATED || method == ZipMethod.ENHANCED_DEFLATED.getCode() || ZipMethod.isZstd(method)
                 || method == ZipMethod.XZ.getCode();
     }
 }
diff --git a/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStreamMalformedTest.java b/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStreamMalformedTest.java
new file mode 100644
index 000000000..5dec0aef9
--- /dev/null
+++ b/src/test/java/org/apache/commons/compress/archivers/zip/ZipArchiveInputStreamMalformedTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.commons.compress.archivers.zip;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.apache.commons.compress.CompressException;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link ZipArchiveInputStream}.
+ */
+public class ZipArchiveInputStreamMalformedTest {
+
+    /**
+     * If you want to see a MemoryLimitException, then run with {@code -Xmx64m}.
+     */
+    @Test
+    void test() throws IOException {
+        final String fixture = "target/ZipArchiveInputStreamMalformedTest.bin";
+        try (FileOutputStream fos = new FileOutputStream(fixture)) {
+            // Local File Header (LFH).
+            final ByteBuffer buffer = ByteBuffer.allocate(30); // size of LFH without filename/extra.
+            buffer.order(ByteOrder.LITTLE_ENDIAN);
+            buffer.putInt(0x04034B50); // Local file header signature.
+            buffer.putShort((short) 20); // version needed to extract.
+            buffer.putShort((short) 0x08); // general purpose bit flag (bit 3 set).
+            buffer.putShort((short) 0); // compression method.
+            buffer.putShort((short) 0); // last mod file time.
+            buffer.putShort((short) 0); // last mod file date.
+            buffer.putInt(0); // CRC-32.
+            buffer.putInt(0); // compressed size.
+            buffer.putInt(0); // uncompressed size.
+            buffer.putShort((short) 1); // file name length.
+            buffer.putShort((short) 0); // extra field length.
+            fos.write(buffer.array());
+            // Filename: "a".
+            fos.write('a');
+            // Payload: 110 MB of 'A'.
+            final byte[] chunk = new byte[1024 * 1024]; // 1 MB chunk.
+            Arrays.fill(chunk, (byte) 'A');
+            for (int i = 0; i < 110; i++) {
+                fos.write(chunk);
+            }
+        }
+        try (ZipArchiveInputStream zis = new ZipArchiveInputStream(new FileInputStream(fixture), StandardCharsets.UTF_8.name(), true, true)) {
+            zis.getNextEntry();
+            assertThrows(CompressException.class, () -> IOUtils.toByteArray(zis));
+        }
+    }
+}