/*
 * Copyright 2017 The gRPC Authors
 *
 * Licensed 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 io.grpc.internal;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.common.io.ByteStreams;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for {@link GzipInflatingBuffer}. */
@RunWith(JUnit4.class)
public class GzipInflatingBufferTest {
  private static final String UNCOMPRESSABLE_FILE = "/io/grpc/internal/uncompressable.bin";

  private static final int GZIP_HEADER_MIN_SIZE = 10;
  private static final int GZIP_TRAILER_SIZE = 8;
  private static final int GZIP_HEADER_FLAG_INDEX = 3;

  public static final int GZIP_MAGIC = 0x8b1f;

  private static final int FTEXT = 1;
  private static final int FHCRC = 2;
  private static final int FEXTRA = 4;
  private static final int FNAME = 8;
  private static final int FCOMMENT = 16;

  private static final int TRUNCATED_DATA_SIZE = 10;

  private byte[] originalData;
  private byte[] gzippedData;
  private byte[] gzipHeader;
  private byte[] deflatedBytes;
  private byte[] gzipTrailer;
  private byte[] truncatedData;
  private byte[] gzippedTruncatedData;

  private GzipInflatingBuffer gzipInflatingBuffer;

  @Before
  public void setUp() {
    gzipInflatingBuffer = new GzipInflatingBuffer();
    try {
      originalData = ByteStreams.toByteArray(getClass().getResourceAsStream(UNCOMPRESSABLE_FILE));
      truncatedData = Arrays.copyOf(originalData, TRUNCATED_DATA_SIZE);

      ByteArrayOutputStream gzippedOutputStream = new ByteArrayOutputStream();
      OutputStream gzippingOutputStream = new GZIPOutputStream(gzippedOutputStream);
      gzippingOutputStream.write(originalData);
      gzippingOutputStream.close();
      gzippedData = gzippedOutputStream.toByteArray();
      gzippedOutputStream.close();

      gzipHeader = Arrays.copyOf(gzippedData, GZIP_HEADER_MIN_SIZE);
      deflatedBytes =
          Arrays.copyOfRange(
              gzippedData, GZIP_HEADER_MIN_SIZE, gzippedData.length - GZIP_TRAILER_SIZE);
      gzipTrailer =
          Arrays.copyOfRange(
              gzippedData, gzippedData.length - GZIP_TRAILER_SIZE, gzippedData.length);

      ByteArrayOutputStream truncatedGzippedOutputStream = new ByteArrayOutputStream();
      OutputStream smallerGzipCompressingStream =
          new GZIPOutputStream(truncatedGzippedOutputStream);
      smallerGzipCompressingStream.write(truncatedData);
      smallerGzipCompressingStream.close();
      gzippedTruncatedData = truncatedGzippedOutputStream.toByteArray();
      truncatedGzippedOutputStream.close();
    } catch (Exception e) {
      throw new RuntimeException("Failed to set up compressed data", e);
    }
  }

  @After
  public void tearDown() {
    gzipInflatingBuffer.close();
  }

  @Test
  public void gzipInflateWorks() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(gzippedData.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void splitGzipStreamWorks() throws Exception {
    int initialBytes = 100;
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData, 0, initialBytes));

    byte[] b = new byte[originalData.length];
    int n = gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
    assertTrue("inflated bytes expected", n > 0);
    assertTrue("gzipInflatingBuffer is not stalled", gzipInflatingBuffer.isStalled());
    assertEquals(initialBytes, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(
        ReadableBuffers.wrap(gzippedData, initialBytes, gzippedData.length - initialBytes));
    int bytesRemaining = originalData.length - n;
    assertEquals(bytesRemaining, gzipInflatingBuffer.inflateBytes(b, n, bytesRemaining));
    assertEquals(gzippedData.length - initialBytes, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void inflateBytesObeysOffsetAndLength() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));

    int offset = 10;
    int length = 100;
    byte[] b = new byte[offset + length + offset];
    assertEquals(length, gzipInflatingBuffer.inflateBytes(b, offset, length));
    assertTrue(
        "bytes written before offset",
        Arrays.equals(new byte[offset], Arrays.copyOfRange(b, 0, offset)));
    assertTrue(
        "inflated data does not match",
        Arrays.equals(
            Arrays.copyOfRange(originalData, 0, length),
            Arrays.copyOfRange(b, offset, offset + length)));
    assertTrue(
        "bytes written beyond length",
        Arrays.equals(
            new byte[offset], Arrays.copyOfRange(b, offset + length, offset + length + offset)));
  }

  @Test
  public void concatenatedStreamsWorks() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedTruncatedData));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedTruncatedData));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(gzippedData.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));

    assertEquals(
        truncatedData.length, gzipInflatingBuffer.inflateBytes(b, 0, truncatedData.length));
    assertEquals(gzippedTruncatedData.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));

    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(gzippedData.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));

    assertEquals(
        truncatedData.length, gzipInflatingBuffer.inflateBytes(b, 0, truncatedData.length));
    assertEquals(gzippedTruncatedData.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void requestingTooManyBytesStillReturnsEndOfBlock() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));

    int len = 2 * originalData.length;
    byte[] b = new byte[len];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, len));
    assertEquals(gzippedData.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue(gzipInflatingBuffer.isStalled());
    assertTrue(
        "inflated data does not match",
        Arrays.equals(originalData, Arrays.copyOf(b, originalData.length)));
  }

  @Test
  public void closeStopsDecompression() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));

    byte[] b = new byte[1];
    gzipInflatingBuffer.inflateBytes(b, 0, 1);
    gzipInflatingBuffer.close();
    try {
      gzipInflatingBuffer.inflateBytes(b, 0, 1);
      fail("Expected IllegalStateException");
    } catch (IllegalStateException expectedException) {
      assertEquals("GzipInflatingBuffer is closed", expectedException.getMessage());
    }
  }

  @Test
  public void isStalledReturnsTrueAtEndOfStream() throws Exception {
    int bytesToWithhold = 10;

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));

    byte[] b = new byte[originalData.length];
    gzipInflatingBuffer.inflateBytes(b, 0, originalData.length - bytesToWithhold);
    assertFalse("gzipInflatingBuffer is stalled", gzipInflatingBuffer.isStalled());

    gzipInflatingBuffer.inflateBytes(b, originalData.length - bytesToWithhold, bytesToWithhold);
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
    assertTrue("gzipInflatingBuffer is not stalled", gzipInflatingBuffer.isStalled());
  }

  @Test
  public void isStalledReturnsFalseBetweenStreams() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
    assertFalse("gzipInflatingBuffer is stalled", gzipInflatingBuffer.isStalled());

    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
    assertTrue("gzipInflatingBuffer is not stalled", gzipInflatingBuffer.isStalled());
  }

  @Test
  public void isStalledReturnsFalseBetweenSmallStreams() throws Exception {
    // Use small streams to make sure that they all fit in the inflater buffer
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedTruncatedData));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedTruncatedData));

    byte[] b = new byte[truncatedData.length];
    assertEquals(
        truncatedData.length, gzipInflatingBuffer.inflateBytes(b, 0, truncatedData.length));
    assertTrue("inflated data does not match", Arrays.equals(truncatedData, b));
    assertFalse("gzipInflatingBuffer is stalled", gzipInflatingBuffer.isStalled());

    assertEquals(
        truncatedData.length, gzipInflatingBuffer.inflateBytes(b, 0, truncatedData.length));
    assertTrue("inflated data does not match", Arrays.equals(truncatedData, b));
    assertTrue("gzipInflatingBuffer is not stalled", gzipInflatingBuffer.isStalled());
  }

  @Test
  public void isStalledReturnsTrueWithPartialNextHeaderAvailable() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedTruncatedData));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(new byte[1]));

    byte[] b = new byte[truncatedData.length];
    assertEquals(
        truncatedData.length, gzipInflatingBuffer.inflateBytes(b, 0, truncatedData.length));
    assertTrue("inflated data does not match", Arrays.equals(truncatedData, b));
    assertTrue("gzipInflatingBuffer is not stalled", gzipInflatingBuffer.isStalled());
    assertTrue("partial data expected", gzipInflatingBuffer.hasPartialData());
  }

  @Test
  public void isStalledWorksWithAllHeaderFlags() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] =
        (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FTEXT | FHCRC | FEXTRA | FNAME | FCOMMENT);
    int len = 1025;
    byte[] fExtraLen = {(byte) len, (byte) (len >> 8)};
    byte[] fExtra = new byte[len];
    byte[] zeroTerminatedBytes = new byte[len];
    for (int i = 0; i < len - 1; i++) {
      zeroTerminatedBytes[i] = 1;
    }
    ByteArrayOutputStream newHeader = new ByteArrayOutputStream();
    newHeader.write(gzipHeader);
    newHeader.write(fExtraLen);
    newHeader.write(fExtra);
    newHeader.write(zeroTerminatedBytes); // FNAME
    newHeader.write(zeroTerminatedBytes); // FCOMMENT
    byte[] headerCrc16 = getHeaderCrc16Bytes(newHeader.toByteArray());

    assertTrue("gzipInflatingBuffer is not stalled", gzipInflatingBuffer.isStalled());

    addInTwoChunksAndVerifyIsStalled(gzipHeader);
    addInTwoChunksAndVerifyIsStalled(fExtraLen);
    addInTwoChunksAndVerifyIsStalled(fExtra);
    addInTwoChunksAndVerifyIsStalled(zeroTerminatedBytes);
    addInTwoChunksAndVerifyIsStalled(zeroTerminatedBytes);
    addInTwoChunksAndVerifyIsStalled(headerCrc16);

    byte[] b = new byte[originalData.length];
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));

    addInTwoChunksAndVerifyIsStalled(gzipTrailer);
  }

  @Test
  public void hasPartialData() throws Exception {
    assertFalse("no partial data expected", gzipInflatingBuffer.hasPartialData());
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(new byte[1]));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
    assertTrue("partial data expected", gzipInflatingBuffer.hasPartialData());
  }

  @Test
  public void hasPartialDataWithoutGzipTrailer() throws Exception {
    assertFalse("no partial data expected", gzipInflatingBuffer.hasPartialData());
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
    assertTrue("partial data expected", gzipInflatingBuffer.hasPartialData());
  }

  @Test
  public void inflatingCompleteGzipStreamConsumesTrailer() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
    assertFalse("no partial data expected", gzipInflatingBuffer.hasPartialData());
  }

  @Test
  public void bytesConsumedForPartiallyInflatedBlock() throws Exception {
    int bytesToWithhold = 1;
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedTruncatedData));

    byte[] b = new byte[truncatedData.length];
    assertEquals(
        truncatedData.length - bytesToWithhold,
        gzipInflatingBuffer.inflateBytes(b, 0, truncatedData.length - bytesToWithhold));
    assertEquals(
        gzippedTruncatedData.length - bytesToWithhold - GZIP_TRAILER_SIZE,
        gzipInflatingBuffer.getAndResetBytesConsumed());
    assertEquals(
        bytesToWithhold,
        gzipInflatingBuffer.inflateBytes(
            b, truncatedData.length - bytesToWithhold, bytesToWithhold));
    assertEquals(
        bytesToWithhold + GZIP_TRAILER_SIZE, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(truncatedData, b));
  }

  @Test
  public void getAndResetCompressedBytesConsumedReportsHeaderFlagBytes() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] =
        (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FTEXT | FHCRC | FEXTRA | FNAME | FCOMMENT);
    int len = 1025;
    byte[] fExtraLen = {(byte) len, (byte) (len >> 8)};
    byte[] fExtra = new byte[len];
    byte[] zeroTerminatedBytes = new byte[len];
    for (int i = 0; i < len - 1; i++) {
      zeroTerminatedBytes[i] = 1;
    }
    ByteArrayOutputStream newHeader = new ByteArrayOutputStream();
    newHeader.write(gzipHeader);
    newHeader.write(fExtraLen);
    newHeader.write(fExtra);
    newHeader.write(zeroTerminatedBytes); // FNAME
    newHeader.write(zeroTerminatedBytes); // FCOMMENT
    byte[] headerCrc16 = getHeaderCrc16Bytes(newHeader.toByteArray());

    byte[] b = new byte[originalData.length];
    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertEquals(0, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertEquals(gzipHeader.length, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(fExtraLen));
    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertEquals(fExtraLen.length, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(fExtra));
    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertEquals(fExtra.length, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(zeroTerminatedBytes));
    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertEquals(zeroTerminatedBytes.length, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(zeroTerminatedBytes));
    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertEquals(zeroTerminatedBytes.length, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(headerCrc16));
    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertEquals(headerCrc16.length, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(deflatedBytes.length, gzipInflatingBuffer.getAndResetBytesConsumed());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));
    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertEquals(gzipTrailer.length, gzipInflatingBuffer.getAndResetBytesConsumed());
  }

  @Test
  public void getAndResetDeflatedBytesConsumedExcludesGzipMetadata() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzippedData));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(
        gzippedData.length - GZIP_HEADER_MIN_SIZE - GZIP_TRAILER_SIZE,
        gzipInflatingBuffer.getAndResetDeflatedBytesConsumed());
  }

  @Test
  public void wrongHeaderMagicShouldFail() throws Exception {
    gzipHeader[1] = (byte) ~(GZIP_MAGIC >> 8);
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    try {
      byte[] b = new byte[1];
      gzipInflatingBuffer.inflateBytes(b, 0, 1);
      fail("Expected ZipException");
    } catch (ZipException expectedException) {
      assertEquals("Not in GZIP format", expectedException.getMessage());
    }
  }

  @Test
  public void wrongHeaderCompressionMethodShouldFail() throws Exception {
    gzipHeader[2] = 7; // Should be 8
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    try {
      byte[] b = new byte[1];
      gzipInflatingBuffer.inflateBytes(b, 0, 1);
      fail("Expected ZipException");
    } catch (ZipException expectedException) {
      assertEquals("Unsupported compression method", expectedException.getMessage());
    }
  }

  @Test
  public void allHeaderFlagsWork() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] =
        (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FTEXT | FHCRC | FEXTRA | FNAME | FCOMMENT);
    int len = 1025;
    byte[] fExtraLen = {(byte) len, (byte) (len >> 8)};
    byte[] fExtra = new byte[len];
    byte[] zeroTerminatedBytes = new byte[len];
    for (int i = 0; i < len - 1; i++) {
      zeroTerminatedBytes[i] = 1;
    }
    ByteArrayOutputStream newHeader = new ByteArrayOutputStream();
    newHeader.write(gzipHeader);
    newHeader.write(fExtraLen);
    newHeader.write(fExtra);
    newHeader.write(zeroTerminatedBytes); // FNAME
    newHeader.write(zeroTerminatedBytes); // FCOMMENT
    byte[] headerCrc16 = getHeaderCrc16Bytes(newHeader.toByteArray());
    newHeader.write(headerCrc16);

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(newHeader.toByteArray()));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    byte[] b = new byte[originalData.length];
    gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void headerFTextFlagIsIgnored() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FTEXT);
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(gzippedData.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void headerFhcrcFlagWorks() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FHCRC);

    byte[] headerCrc16 = getHeaderCrc16Bytes(gzipHeader);

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(headerCrc16));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(
        gzippedData.length + headerCrc16.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void headerInvalidFhcrcFlagFails() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FHCRC);

    byte[] headerCrc16 = getHeaderCrc16Bytes(gzipHeader);
    headerCrc16[0] = (byte) ~headerCrc16[0];

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(headerCrc16));
    try {
      byte[] b = new byte[1];
      gzipInflatingBuffer.inflateBytes(b, 0, 1);
      fail("Expected ZipException");
    } catch (ZipException expectedException) {
      assertEquals("Corrupt GZIP header", expectedException.getMessage());
    }
  }

  @Test
  public void headerFExtraFlagWorks() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FEXTRA);

    int len = 1025;
    byte[] fExtraLen = {(byte) len, (byte) (len >> 8)};
    byte[] fExtra = new byte[len];

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(fExtraLen));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(fExtra));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(
        gzippedData.length + fExtraLen.length + fExtra.length,
        gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void headerFExtraFlagWithZeroLenWorks() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FEXTRA);
    byte[] fExtraLen = new byte[2];

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(fExtraLen));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(
        gzippedData.length + fExtraLen.length, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void headerFExtraFlagWithMissingExtraLenFails() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FEXTRA);

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    try {
      byte[] b = new byte[originalData.length];
      gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
      fail("Expected DataFormatException");
    } catch (DataFormatException expectedException) {
      assertTrue(
          "wrong exception message",
          expectedException.getMessage().startsWith("Inflater data format exception:"));
    }
  }

  @Test
  public void headerFExtraFlagWithMissingExtraBytesFails() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FEXTRA);

    int len = 5;
    byte[] fExtraLen = {(byte) len, (byte) (len >> 8)};

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(fExtraLen));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    try {
      byte[] b = new byte[originalData.length];
      gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
      fail("Expected DataFormatException");
    } catch (DataFormatException expectedException) {
      assertTrue(
          "wrong exception message",
          expectedException.getMessage().startsWith("Inflater data format exception:"));
    }
  }

  @Test
  public void headerFNameFlagWorks() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FNAME);
    int len = 1025;
    byte[] zeroTerminatedBytes = new byte[len];
    for (int i = 0; i < len - 1; i++) {
      zeroTerminatedBytes[i] = 1;
    }

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(zeroTerminatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(gzippedData.length + len, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void headerFNameFlagWithMissingBytesFail() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FNAME);
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    try {
      byte[] b = new byte[originalData.length];
      gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
      fail("Expected DataFormatException");
    } catch (DataFormatException expectedException) {
      assertTrue(
          "wrong exception message",
          expectedException.getMessage().startsWith("Inflater data format exception:"));
    }
  }

  @Test
  public void headerFCommentFlagWorks() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FCOMMENT);
    int len = 1025;
    byte[] zeroTerminatedBytes = new byte[len];
    for (int i = 0; i < len - 1; i++) {
      zeroTerminatedBytes[i] = 1;
    }

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(zeroTerminatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    byte[] b = new byte[originalData.length];
    assertEquals(originalData.length, gzipInflatingBuffer.inflateBytes(b, 0, originalData.length));
    assertEquals(gzippedData.length + len, gzipInflatingBuffer.getAndResetBytesConsumed());
    assertTrue("inflated data does not match", Arrays.equals(originalData, b));
  }

  @Test
  public void headerFCommentFlagWithMissingBytesFail() throws Exception {
    gzipHeader[GZIP_HEADER_FLAG_INDEX] = (byte) (gzipHeader[GZIP_HEADER_FLAG_INDEX] | FCOMMENT);

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));
    try {
      byte[] b = new byte[originalData.length];
      gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
      fail("Expected DataFormatException");
    } catch (DataFormatException expectedException) {
      assertTrue(
          "wrong exception message",
          expectedException.getMessage().startsWith("Inflater data format exception:"));
    }
  }

  @Test
  public void wrongTrailerCrcShouldFail() throws Exception {
    gzipTrailer[0] = (byte) ~gzipTrailer[0];
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    try {
      byte[] b = new byte[originalData.length];
      gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
      fail("Expected ZipException");
    } catch (ZipException expectedException) {
      assertEquals("Corrupt GZIP trailer", expectedException.getMessage());
    }
  }

  @Test
  public void wrongTrailerISizeShouldFail() throws Exception {
    gzipTrailer[GZIP_TRAILER_SIZE - 1] = (byte) ~gzipTrailer[GZIP_TRAILER_SIZE - 1];
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(deflatedBytes));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipTrailer));

    try {
      byte[] b = new byte[originalData.length];
      gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
      fail("Expected ZipException");
    } catch (ZipException expectedException) {
      assertEquals("Corrupt GZIP trailer", expectedException.getMessage());
    }
  }

  @Test
  public void invalidDeflateBlockShouldFail() throws Exception {
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(gzipHeader));
    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(new byte[10]));

    try {
      byte[] b = new byte[originalData.length];
      gzipInflatingBuffer.inflateBytes(b, 0, originalData.length);
      fail("Expected DataFormatException");
    } catch (DataFormatException expectedException) {
      assertTrue(
          "wrong exception message",
          expectedException.getMessage().startsWith("Inflater data format exception:"));
    }
  }

  private void addInTwoChunksAndVerifyIsStalled(byte[] input) throws Exception {
    byte[] b = new byte[1];

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(input, 0, input.length - 1));
    assertFalse("gzipInflatingBuffer is stalled", gzipInflatingBuffer.isStalled());

    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertTrue("gzipInflatingBuffer is not stalled", gzipInflatingBuffer.isStalled());

    gzipInflatingBuffer.addGzippedBytes(ReadableBuffers.wrap(input, input.length - 1, 1));
    assertFalse("gzipInflatingBuffer is stalled", gzipInflatingBuffer.isStalled());

    assertEquals(0, gzipInflatingBuffer.inflateBytes(b, 0, 1));
    assertTrue("gzipInflatingBuffer is not stalled", gzipInflatingBuffer.isStalled());
  }

  private byte[] getHeaderCrc16Bytes(byte[] headerBytes) {
    CRC32 crc = new CRC32();
    crc.update(headerBytes);
    byte[] headerCrc16 = {(byte) crc.getValue(), (byte) (crc.getValue() >> 8)};
    return headerCrc16;
  }
}