docs: public InputStream open() throws IOException, InterruptedException {
          if (recyclableInflaterInstance != null) {
              throw new IOException("Zip entry already open");
          }
          if (isDeflated) {
              recyclableInflaterInstance = nestedJarHandler.inflaterRecycler.acquire();
          }
          return new InputStream() {
              /** The data start offset within the physical zip file. */
              private final long dataStartOffsetWithinPhysicalZipFile = getEntryDataStartOffsetWithinPhysicalZipFile();
  
              /** A scratch buffer. */
              private final byte[] scratch = new byte[8192];
  
              /** The current 2GB chunk of the zip entry. */
              private ByteBuffer currChunkByteBuf;
  
              /** True if the current 2GB chunk is the last chunk in the zip entry. */
              private boolean isLastChunk;
  
              /** The index of the current 2GB chunk. */
              private int currChunkIdx;
  
              /** True if the end of the zip entry has been reached. */
              private boolean eof;
  
              /** The {@link Inflater} instance, or null if the entry is stored rather than deflated. */
              private final Inflater inflater = isDeflated ? recyclableInflaterInstance.getInflater() : null;
  
              /** True if this {@link InputStream} has been closed. */
              private final AtomicBoolean closed = new AtomicBoolean(false);
  
              /** The size of the {@link Inflate} buffer to use. */
              private static final int INFLATE_BUF_SIZE = 1024;
  
              // Open the first 2GB chunk.
              {
                  // Calculate the chunk index for the first chunk
                  currChunkIdx = (int) (dataStartOffsetWithinPhysicalZipFile / FileUtils.MAX_BUFFER_SIZE);
  
                  // Get the MappedByteBuffer for the 2GB chunk, and duplicate it
                  currChunkByteBuf = parentLogicalZipFile.physicalZipFile.getByteBuffer(currChunkIdx).duplicate();
  
                  // Calculate the start position within the first chunk, and set the position of the slice.
                  // N.B. the cast to Buffer is necessary, see:
                  // https://github.com/plasma-umass/doppio/issues/497#issuecomment-334740243
                  // https://github.com/classgraph/classgraph/issues/284#issuecomment-443612800
                  final int chunkPos = (int) (dataStartOffsetWithinPhysicalZipFile
                          - (((long) currChunkIdx) * (long) FileUtils.MAX_BUFFER_SIZE));
                  ((Buffer) currChunkByteBuf).position(chunkPos);
  
                  // Calculate end pos for the first chunk, and truncate it if it overflows 2GB
                  final long endPos = chunkPos + compressedSize;
                  ((Buffer) currChunkByteBuf).limit((int) Math.min(FileUtils.MAX_BUFFER_SIZE, endPos));
                  isLastChunk = endPos <= FileUtils.MAX_BUFFER_SIZE;
              }
  
              /** Advance to the next 2GB chunk. */
              private boolean readNextChunk() throws IOException, InterruptedException {
                  currChunkIdx++;
                  if (currChunkIdx >= parentLogicalZipFile.physicalZipFile.numMappedByteBuffers) {
                      // Ran out of chunks
                      return false;
                  }
  
                  // Calculate how many bytes were consumed in previous chunks
                  final long chunkStartOff = ((long) currChunkIdx) * (long) FileUtils.MAX_BUFFER_SIZE;
                  final long priorBytes = chunkStartOff - dataStartOffsetWithinPhysicalZipFile;
                  final long remainingBytes = compressedSize - priorBytes;
                  if (remainingBytes <= 0) {
                      return false;
                  }
  
                  // Get the MappedByteBuffer for the next 2GB chunk, and duplicate it
                  currChunkByteBuf = parentLogicalZipFile.physicalZipFile.getByteBuffer(currChunkIdx).duplicate();
  
                  // The start position for 2nd and subsequent chunks is 0.
                  // N.B. the cast to Buffer is necessary, see:
                  // https://github.com/plasma-umass/doppio/issues/497#issuecomment-334740243
                  // https://github.com/classgraph/classgraph/issues/284#issuecomment-443612800
                  ((Buffer) currChunkByteBuf).position(0);
  
                  // Calculate end pos for the next chunk, and truncate it if it overflows 2GB
                  ((Buffer) currChunkByteBuf).limit((int) Math.min(FileUtils.MAX_BUFFER_SIZE, remainingBytes));
                  isLastChunk = remainingBytes <= FileUtils.MAX_BUFFER_SIZE;
                  return true;
              }
  
              /**
               * Inflate deflated data.
               *
               * @param buf
               *            the buffer to inflate into.
               * @param off
               *            the offset within buf to start writing.
               * @param len
               *            the number of bytes of uncompressed data to read.
               * @return the number of bytes read.
               * @throws IOException
               *             if an I/O exception occurred.
               * @throws InterruptedException
               *             if the thread was interrupted.
               */
              private int readDeflated(final byte[] buf, final int off, final int len)
                      throws IOException, InterruptedException {
                  try {
                      final byte[] inflateBuf = new byte[INFLATE_BUF_SIZE];
                      int numInflatedBytes;
                      while ((numInflatedBytes = inflater.inflate(buf, off, len)) == 0) {
                          if (inflater.finished() || inflater.needsDictionary()) {
                              eof = true;
                              return -1;
                          }
                          if (inflater.needsInput()) {
                              // Check if there's still data left in the current chunk
                              if (!currChunkByteBuf.hasRemaining()
                                      // No more bytes in current chunk -- get next chunk, and then make sure
                                      // that currChunkByteBuf.hasRemaining() subsequently returns true
                                      && !(readNextChunk() && currChunkByteBuf.hasRemaining())) {
                                  // Ran out of data in the current chunk, and could not read a new chunk
                                  throw new IOException("Unexpected EOF in deflated data");
                              }
                              // Set inflater input for the current chunk
  
                              // In JDK11+: simply use the following instead of all the lines below:
                              //     inflater.setInput(currChunkByteBuf);
                              // N.B. the ByteBuffer version of setInput doesn't seem to need the extra
                              // padding byte at the end when using the "nowrap" Inflater option.
  
                              // Copy from the ByteBuffer into a temporary byte[] array (needed for JDK<11). 
                              try {
                                  final int remaining = currChunkByteBuf.remaining();
                                  if (isLastChunk && remaining < inflateBuf.length) {
                                      // An extra dummy byte is needed at the end of the input stream when
                                      // using the "nowrap" Inflater option.
                                      // See: ZipFile.ZipFileInputStream.fill()
                                      currChunkByteBuf.get(inflateBuf, 0, remaining);
                                      inflateBuf[remaining] = (byte) 0;
                                      inflater.setInput(inflateBuf, 0, remaining + 1);
                                  } else if (isLastChunk && remaining == inflateBuf.length) {
                                      // If this is the last chunk to read, and the number of remaining
                                      // bytes is exactly the size of the buffer, read one byte fewer than
                                      // the number of remaining bytes, to cause the last byte to be read
                                      // in an extra pass.
                                      currChunkByteBuf.get(inflateBuf, 0, remaining - 1);
                                      inflater.setInput(inflateBuf, 0, remaining - 1);
                                  } else {
                                      // There are more than inflateBuf.length bytes remaining to be read,
                                      // or this is not the last chunk (i.e. read all remaining bytes in
                                      // this chunk, which will trigger the next chunk to be read on the
                                      // next loop iteration)
                                      final int bytesToRead = Math.min(inflateBuf.length, remaining);
                                      currChunkByteBuf.get(inflateBuf, 0, bytesToRead);
                                      inflater.setInput(inflateBuf, 0, bytesToRead);
                                  }
                              } catch (final BufferUnderflowException e) {
                                  // Should not happen
                                  throw new IOException("Unexpected EOF in deflated data");
                              }
                          }
                      }
                      return numInflatedBytes;
                  } catch (final DataFormatException e) {
                      throw new ZipException(
                              e.getMessage() != null ? e.getMessage() : "Invalid deflated zip entry data");
                  }
              }
  
              /**
               * Copy stored (non-deflated) data from ByteBuffer to target buffer.
               *
               * @param buf
               *            the buffer to copy the stored entry into.
               * @param off
               *            the offset within buf to start writing.
               * @param len
               *            the number of bytes to read.
               * @return the number of bytes read.
               * @throws IOException
               *             if an I/O exception occurred.
               * @throws InterruptedException
               *             if the thread was interrupted.
               */
              private int readStored(final byte[] buf, final int off, final int len)
                      throws IOException, InterruptedException {
                  int read = 0;
                  while (read < len) {
                      if (!currChunkByteBuf.hasRemaining() && !readNextChunk()) {
                          return read == 0 ? -1 : read;
                      }
                      final int remainingToRead = len - read;
                      final int remainingInBuf = currChunkByteBuf.remaining();
                      final int numBytesRead = Math.min(remainingToRead, remainingInBuf);
                      try {
                          currChunkByteBuf.get(buf, off + read, numBytesRead);
                      } catch (final BufferUnderflowException e) {
                          // Should not happen
                          throw new EOFException("Unexpected EOF in stored (non-deflated) zip entry data");
                      }
                      read += numBytesRead;
                  }
                  return read;
              }
  
              @Override
              public int read(final byte[] buf, final int off, final int len) throws IOException {
                  if (closed.get()) {
                      throw new IOException("Stream closed");
                  }
                  if (buf == null) {
                      throw new NullPointerException();
                  } else if (off < 0 || len < 0 || len > buf.length - off) {
                      throw new IndexOutOfBoundsException();
                  } else if (len == 0) {
                      return 0;
                  } else if (parentLogicalZipFile.physicalZipFile.fileLen == 0) {
                      return -1;
                  }
                  try {
                      if (isDeflated) {
                          return readDeflated(buf, off, len);
                      } else {
                          return readStored(buf, off, len);
                      }
                  } catch (final InterruptedException e) {
                      nestedJarHandler.interruptionChecker.interrupt();
                      throw new IOException("Thread was interrupted");
                  }
              }
  
              @Override
              public int read() throws IOException {
                  if (closed.get()) {
                      throw new IOException("Stream closed");
                  }
                  return read(scratch, 0, 1) == -1 ? -1 : scratch[0] & 0xff;
              }
  
              @Override
              public int available() throws IOException {
                  if (closed.get()) {
                      throw new IOException("Stream closed");
                  }
                  if (inflater.finished()) {
                      eof = true;
                  }
                  return eof ? 0 : 1;
              }
  
              @Override
              public long skip(final long n) throws IOException {
                  if (closed.get()) {
                      throw new IOException("Stream closed");
                  }
                  if (n < 0) {
                      throw new IllegalArgumentException("Invalid skip value");
                  }
                  long total = 0;
                  while (total < n) {
                      final int numSkipped = read(scratch, 0, (int) Math.min(n - total, scratch.length));
                      if (numSkipped == -1) {
                          eof = true;
                          break;
                      }
                      total += numSkipped;
                  }
                  return total;
              }
  
              @Override
              public boolean markSupported() {
                  return false;
              }
  
              @Override
              public synchronized void mark(final int readlimit) {
                  throw new IllegalArgumentException("Not supported");
              }
  
              @Override
              public synchronized void reset() throws IOException {
                  throw new IllegalArgumentException("Not supported");
              }
  
              @Override
              public void close() throws IOException {
                  if (!closed.getAndSet(true)) {
                      currChunkByteBuf = null;
                      if (recyclableInflaterInstance != null) {
                          // Reset and recycle the Inflater
                          nestedJarHandler.inflaterRecycler.recycle(recyclableInflaterInstance);
                          recyclableInflaterInstance = null;
                      }
                  }
              }
          };
      }