/* * 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 * * http://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.nifi.remote.io; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.zip.DataFormatException; import java.util.zip.Inflater; public class CompressionInputStream extends InputStream { private final InputStream in; private final Inflater inflater; private byte[] compressedBuffer; private byte[] buffer; private int bufferIndex; private boolean eos = false; // whether or not we've reached the end of stream private boolean allDataRead = false; // different from eos b/c eos means allDataRead == true && buffer is empty private final byte[] fourByteBuffer = new byte[4]; public CompressionInputStream(final InputStream in) { this.in = in; inflater = new Inflater(); buffer = new byte[0]; compressedBuffer = new byte[0]; bufferIndex = 1; } private String toHex(final byte[] array) { final StringBuilder sb = new StringBuilder("0x"); for (final byte b : array) { final String hex = Integer.toHexString(b).toUpperCase(); if (hex.length() == 1) { sb.append("0"); } sb.append(hex); } return sb.toString(); } protected void readChunkHeader() throws IOException { // Ensure that we have a valid SYNC chunk fillBuffer(fourByteBuffer); if (!Arrays.equals(CompressionOutputStream.SYNC_BYTES, fourByteBuffer)) { throw new IOException("Invalid CompressionInputStream. Expected first 4 bytes to be 'SYNC' but were " + toHex(fourByteBuffer)); } // determine the size of the decompressed buffer fillBuffer(fourByteBuffer); buffer = new byte[toInt(fourByteBuffer)]; // determine the size of the compressed buffer fillBuffer(fourByteBuffer); compressedBuffer = new byte[toInt(fourByteBuffer)]; bufferIndex = buffer.length; // indicate that buffer is empty } private int toInt(final byte[] data) { return ((data[0] & 0xFF) << 24) | ((data[1] & 0xFF) << 16) | ((data[2] & 0xFF) << 8) | (data[3] & 0xFF); } protected void bufferAndDecompress() throws IOException { if (allDataRead) { eos = true; return; } readChunkHeader(); fillBuffer(compressedBuffer); inflater.setInput(compressedBuffer); try { inflater.inflate(buffer); } catch (final DataFormatException e) { throw new IOException(e); } inflater.reset(); bufferIndex = 0; final int moreDataByte = in.read(); if (moreDataByte < 1) { allDataRead = true; } else if (moreDataByte > 1) { throw new IOException("Expected indicator of whether or not more data was to come (-1, 0, or 1) but got " + moreDataByte); } } private void fillBuffer(final byte[] buffer) throws IOException { int len; int bytesLeft = buffer.length; int bytesRead = 0; while (bytesLeft > 0 && (len = in.read(buffer, bytesRead, bytesLeft)) > 0) { bytesLeft -= len; bytesRead += len; } if (bytesRead < buffer.length) { throw new EOFException(); } } private boolean isBufferEmpty() { return bufferIndex >= buffer.length; } @Override public int read() throws IOException { if (eos) { return -1; } if (isBufferEmpty()) { bufferAndDecompress(); } if (isBufferEmpty()) { eos = true; return -1; } return buffer[bufferIndex++] & 0xFF; } @Override public int read(final byte[] b) throws IOException { return read(b, 0, b.length); } @Override public int read(final byte[] b, final int off, final int len) throws IOException { if (eos) { return -1; } if (isBufferEmpty()) { bufferAndDecompress(); } if (isBufferEmpty()) { eos = true; return -1; } final int free = buffer.length - bufferIndex; final int bytesToTransfer = Math.min(len, free); System.arraycopy(buffer, bufferIndex, b, off, bytesToTransfer); bufferIndex += bytesToTransfer; return bytesToTransfer; } /** * Calls {@link Inflater#end()} to free acquired memory to prevent OutOfMemory error. * However, does NOT close underlying InputStream. * * @throws java.io.IOException for any issues closing underlying stream */ @Override public void close() throws IOException { inflater.end(); } }