diff --git a/NOTICE.txt b/NOTICE.txt
index dd1d88c3e9..c051eacb54 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -211,3 +211,11 @@ non-blocking XML processor, which can be obtained at:
* HOMEPAGE:
* http://wiki.fasterxml.com/AaltoHome
+This product contains a modified version of 'HPACK', a Java implementation of
+the HTTP/2 HPACK algorithm written by Twitter. It can be obtained at:
+
+ * LICENSE:
+ * license/LICENSE.hpack.txt (Apache License 2.0)
+ * HOMEPAGE:
+ * https://github.com/twitter/hpack
+
diff --git a/codec-http2/pom.xml b/codec-http2/pom.xml
index 7b7edbde90..d819aab873 100644
--- a/codec-http2/pom.xml
+++ b/codec-http2/pom.xml
@@ -39,15 +39,15 @@
netty-handler${project.version}
-
- com.twitter
- hpack
- com.jcraftjzlibtrue
+
+ com.google.code.gson
+ gson
+
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java
index 688498c9d0..e0f6cb1254 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java
@@ -15,10 +15,10 @@
package io.netty.handler.codec.http2;
-import com.twitter.hpack.Decoder;
-import com.twitter.hpack.HeaderListener;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
+import io.netty.handler.codec.http2.hpack.Decoder;
+import io.netty.handler.codec.http2.hpack.HeaderListener;
import io.netty.util.AsciiString;
import java.io.IOException;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java
index 33f948479b..854498c3a2 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java
@@ -15,9 +15,9 @@
package io.netty.handler.codec.http2;
-import com.twitter.hpack.Encoder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
+import io.netty.handler.codec.http2.hpack.Encoder;
import io.netty.util.AsciiString;
import java.io.ByteArrayOutputStream;
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Decoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Decoder.java
new file mode 100644
index 0000000000..9b801f2745
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Decoder.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import io.netty.handler.codec.http2.hpack.HpackUtil.IndexType;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public final class Decoder {
+
+ private static final IOException DECOMPRESSION_EXCEPTION =
+ new IOException("decompression failure");
+ private static final IOException ILLEGAL_INDEX_VALUE =
+ new IOException("illegal index value");
+ private static final IOException INVALID_MAX_DYNAMIC_TABLE_SIZE =
+ new IOException("invalid max dynamic table size");
+ private static final IOException MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED =
+ new IOException("max dynamic table size change required");
+
+ private static final byte[] EMPTY = {};
+
+ private final DynamicTable dynamicTable;
+
+ private int maxHeaderSize;
+ private int maxDynamicTableSize;
+ private int encoderMaxDynamicTableSize;
+ private boolean maxDynamicTableSizeChangeRequired;
+
+ private long headerSize;
+ private State state;
+ private IndexType indexType;
+ private int index;
+ private boolean huffmanEncoded;
+ private int skipLength;
+ private int nameLength;
+ private int valueLength;
+ private byte[] name;
+
+ private enum State {
+ READ_HEADER_REPRESENTATION,
+ READ_MAX_DYNAMIC_TABLE_SIZE,
+ READ_INDEXED_HEADER,
+ READ_INDEXED_HEADER_NAME,
+ READ_LITERAL_HEADER_NAME_LENGTH_PREFIX,
+ READ_LITERAL_HEADER_NAME_LENGTH,
+ READ_LITERAL_HEADER_NAME,
+ SKIP_LITERAL_HEADER_NAME,
+ READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX,
+ READ_LITERAL_HEADER_VALUE_LENGTH,
+ READ_LITERAL_HEADER_VALUE,
+ SKIP_LITERAL_HEADER_VALUE
+ }
+
+ /**
+ * Creates a new decoder.
+ */
+ public Decoder(int maxHeaderSize, int maxHeaderTableSize) {
+ dynamicTable = new DynamicTable(maxHeaderTableSize);
+ this.maxHeaderSize = maxHeaderSize;
+ maxDynamicTableSize = maxHeaderTableSize;
+ encoderMaxDynamicTableSize = maxHeaderTableSize;
+ maxDynamicTableSizeChangeRequired = false;
+ reset();
+ }
+
+ private void reset() {
+ headerSize = 0;
+ state = State.READ_HEADER_REPRESENTATION;
+ indexType = IndexType.NONE;
+ }
+
+ /**
+ * Decode the header block into header fields.
+ */
+ public void decode(InputStream in, HeaderListener headerListener) throws IOException {
+ while (in.available() > 0) {
+ switch (state) {
+ case READ_HEADER_REPRESENTATION:
+ byte b = (byte) in.read();
+ if (maxDynamicTableSizeChangeRequired && (b & 0xE0) != 0x20) {
+ // Encoder MUST signal maximum dynamic table size change
+ throw MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED;
+ }
+ if (b < 0) {
+ // Indexed Header Field
+ index = b & 0x7F;
+ if (index == 0) {
+ throw ILLEGAL_INDEX_VALUE;
+ } else if (index == 0x7F) {
+ state = State.READ_INDEXED_HEADER;
+ } else {
+ indexHeader(index, headerListener);
+ }
+ } else if ((b & 0x40) == 0x40) {
+ // Literal Header Field with Incremental Indexing
+ indexType = IndexType.INCREMENTAL;
+ index = b & 0x3F;
+ if (index == 0) {
+ state = State.READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;
+ } else if (index == 0x3F) {
+ state = State.READ_INDEXED_HEADER_NAME;
+ } else {
+ // Index was stored as the prefix
+ readName(index);
+ state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
+ }
+ } else if ((b & 0x20) == 0x20) {
+ // Dynamic Table Size Update
+ index = b & 0x1F;
+ if (index == 0x1F) {
+ state = State.READ_MAX_DYNAMIC_TABLE_SIZE;
+ } else {
+ setDynamicTableSize(index);
+ state = State.READ_HEADER_REPRESENTATION;
+ }
+ } else {
+ // Literal Header Field without Indexing / never Indexed
+ indexType = ((b & 0x10) == 0x10) ? IndexType.NEVER : IndexType.NONE;
+ index = b & 0x0F;
+ if (index == 0) {
+ state = State.READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;
+ } else if (index == 0x0F) {
+ state = State.READ_INDEXED_HEADER_NAME;
+ } else {
+ // Index was stored as the prefix
+ readName(index);
+ state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
+ }
+ }
+ break;
+
+ case READ_MAX_DYNAMIC_TABLE_SIZE:
+ int maxSize = decodeULE128(in);
+ if (maxSize == -1) {
+ return;
+ }
+
+ // Check for numerical overflow
+ if (maxSize > Integer.MAX_VALUE - index) {
+ throw DECOMPRESSION_EXCEPTION;
+ }
+
+ setDynamicTableSize(index + maxSize);
+ state = State.READ_HEADER_REPRESENTATION;
+ break;
+
+ case READ_INDEXED_HEADER:
+ int headerIndex = decodeULE128(in);
+ if (headerIndex == -1) {
+ return;
+ }
+
+ // Check for numerical overflow
+ if (headerIndex > Integer.MAX_VALUE - index) {
+ throw DECOMPRESSION_EXCEPTION;
+ }
+
+ indexHeader(index + headerIndex, headerListener);
+ state = State.READ_HEADER_REPRESENTATION;
+ break;
+
+ case READ_INDEXED_HEADER_NAME:
+ // Header Name matches an entry in the Header Table
+ int nameIndex = decodeULE128(in);
+ if (nameIndex == -1) {
+ return;
+ }
+
+ // Check for numerical overflow
+ if (nameIndex > Integer.MAX_VALUE - index) {
+ throw DECOMPRESSION_EXCEPTION;
+ }
+
+ readName(index + nameIndex);
+ state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
+ break;
+
+ case READ_LITERAL_HEADER_NAME_LENGTH_PREFIX:
+ b = (byte) in.read();
+ huffmanEncoded = (b & 0x80) == 0x80;
+ index = b & 0x7F;
+ if (index == 0x7f) {
+ state = State.READ_LITERAL_HEADER_NAME_LENGTH;
+ } else {
+ nameLength = index;
+
+ // Disallow empty names -- they cannot be represented in HTTP/1.x
+ if (nameLength == 0) {
+ throw DECOMPRESSION_EXCEPTION;
+ }
+
+ // Check name length against max header size
+ if (exceedsMaxHeaderSize(nameLength)) {
+
+ if (indexType == IndexType.NONE) {
+ // Name is unused so skip bytes
+ name = EMPTY;
+ skipLength = nameLength;
+ state = State.SKIP_LITERAL_HEADER_NAME;
+ break;
+ }
+
+ // Check name length against max dynamic table size
+ if (nameLength + HeaderField.HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {
+ dynamicTable.clear();
+ name = EMPTY;
+ skipLength = nameLength;
+ state = State.SKIP_LITERAL_HEADER_NAME;
+ break;
+ }
+ }
+ state = State.READ_LITERAL_HEADER_NAME;
+ }
+ break;
+
+ case READ_LITERAL_HEADER_NAME_LENGTH:
+ // Header Name is a Literal String
+ nameLength = decodeULE128(in);
+ if (nameLength == -1) {
+ return;
+ }
+
+ // Check for numerical overflow
+ if (nameLength > Integer.MAX_VALUE - index) {
+ throw DECOMPRESSION_EXCEPTION;
+ }
+ nameLength += index;
+
+ // Check name length against max header size
+ if (exceedsMaxHeaderSize(nameLength)) {
+ if (indexType == IndexType.NONE) {
+ // Name is unused so skip bytes
+ name = EMPTY;
+ skipLength = nameLength;
+ state = State.SKIP_LITERAL_HEADER_NAME;
+ break;
+ }
+
+ // Check name length against max dynamic table size
+ if (nameLength + HeaderField.HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {
+ dynamicTable.clear();
+ name = EMPTY;
+ skipLength = nameLength;
+ state = State.SKIP_LITERAL_HEADER_NAME;
+ break;
+ }
+ }
+ state = State.READ_LITERAL_HEADER_NAME;
+ break;
+
+ case READ_LITERAL_HEADER_NAME:
+ // Wait until entire name is readable
+ if (in.available() < nameLength) {
+ return;
+ }
+
+ name = readStringLiteral(in, nameLength);
+
+ state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
+ break;
+
+ case SKIP_LITERAL_HEADER_NAME:
+ skipLength -= in.skip(skipLength);
+
+ if (skipLength == 0) {
+ state = State.READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
+ }
+ break;
+
+ case READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX:
+ b = (byte) in.read();
+ huffmanEncoded = (b & 0x80) == 0x80;
+ index = b & 0x7F;
+ if (index == 0x7f) {
+ state = State.READ_LITERAL_HEADER_VALUE_LENGTH;
+ } else {
+ valueLength = index;
+
+ // Check new header size against max header size
+ long newHeaderSize = (long) nameLength + (long) valueLength;
+ if (exceedsMaxHeaderSize(newHeaderSize)) {
+ // truncation will be reported during endHeaderBlock
+ headerSize = maxHeaderSize + 1;
+
+ if (indexType == IndexType.NONE) {
+ // Value is unused so skip bytes
+ state = State.SKIP_LITERAL_HEADER_VALUE;
+ break;
+ }
+
+ // Check new header size against max dynamic table size
+ if (newHeaderSize + HeaderField.HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {
+ dynamicTable.clear();
+ state = State.SKIP_LITERAL_HEADER_VALUE;
+ break;
+ }
+ }
+
+ if (valueLength == 0) {
+ insertHeader(headerListener, name, EMPTY, indexType);
+ state = State.READ_HEADER_REPRESENTATION;
+ } else {
+ state = State.READ_LITERAL_HEADER_VALUE;
+ }
+ }
+
+ break;
+
+ case READ_LITERAL_HEADER_VALUE_LENGTH:
+ // Header Value is a Literal String
+ valueLength = decodeULE128(in);
+ if (valueLength == -1) {
+ return;
+ }
+
+ // Check for numerical overflow
+ if (valueLength > Integer.MAX_VALUE - index) {
+ throw DECOMPRESSION_EXCEPTION;
+ }
+ valueLength += index;
+
+ // Check new header size against max header size
+ long newHeaderSize = (long) nameLength + (long) valueLength;
+ if (newHeaderSize + headerSize > maxHeaderSize) {
+ // truncation will be reported during endHeaderBlock
+ headerSize = maxHeaderSize + 1;
+
+ if (indexType == IndexType.NONE) {
+ // Value is unused so skip bytes
+ state = State.SKIP_LITERAL_HEADER_VALUE;
+ break;
+ }
+
+ // Check new header size against max dynamic table size
+ if (newHeaderSize + HeaderField.HEADER_ENTRY_OVERHEAD > dynamicTable.capacity()) {
+ dynamicTable.clear();
+ state = State.SKIP_LITERAL_HEADER_VALUE;
+ break;
+ }
+ }
+ state = State.READ_LITERAL_HEADER_VALUE;
+ break;
+
+ case READ_LITERAL_HEADER_VALUE:
+ // Wait until entire value is readable
+ if (in.available() < valueLength) {
+ return;
+ }
+
+ byte[] value = readStringLiteral(in, valueLength);
+ insertHeader(headerListener, name, value, indexType);
+ state = State.READ_HEADER_REPRESENTATION;
+ break;
+
+ case SKIP_LITERAL_HEADER_VALUE:
+ valueLength -= in.skip(valueLength);
+
+ if (valueLength == 0) {
+ state = State.READ_HEADER_REPRESENTATION;
+ }
+ break;
+
+ default:
+ throw new IllegalStateException("should not reach here");
+ }
+ }
+ }
+
+ /**
+ * End the current header block. Returns if the header field has been truncated. This must be
+ * called after the header block has been completely decoded.
+ */
+ public boolean endHeaderBlock() {
+ boolean truncated = headerSize > maxHeaderSize;
+ reset();
+ return truncated;
+ }
+
+ /**
+ * Set the maximum table size. If this is below the maximum size of the dynamic table used by
+ * the encoder, the beginning of the next header block MUST signal this change.
+ */
+ public void setMaxHeaderTableSize(int maxHeaderTableSize) {
+ maxDynamicTableSize = maxHeaderTableSize;
+ if (maxDynamicTableSize < encoderMaxDynamicTableSize) {
+ // decoder requires less space than encoder
+ // encoder MUST signal this change
+ maxDynamicTableSizeChangeRequired = true;
+ dynamicTable.setCapacity(maxDynamicTableSize);
+ }
+ }
+
+ /**
+ * Return the maximum table size. This is the maximum size allowed by both the encoder and the
+ * decoder.
+ */
+ public int getMaxHeaderTableSize() {
+ return dynamicTable.capacity();
+ }
+
+ /**
+ * Return the number of header fields in the dynamic table. Exposed for testing.
+ */
+ int length() {
+ return dynamicTable.length();
+ }
+
+ /**
+ * Return the size of the dynamic table. Exposed for testing.
+ */
+ int size() {
+ return dynamicTable.size();
+ }
+
+ /**
+ * Return the header field at the given index. Exposed for testing.
+ */
+ HeaderField getHeaderField(int index) {
+ return dynamicTable.getEntry(index + 1);
+ }
+
+ private void setDynamicTableSize(int dynamicTableSize) throws IOException {
+ if (dynamicTableSize > maxDynamicTableSize) {
+ throw INVALID_MAX_DYNAMIC_TABLE_SIZE;
+ }
+ encoderMaxDynamicTableSize = dynamicTableSize;
+ maxDynamicTableSizeChangeRequired = false;
+ dynamicTable.setCapacity(dynamicTableSize);
+ }
+
+ private void readName(int index) throws IOException {
+ if (index <= StaticTable.length) {
+ HeaderField headerField = StaticTable.getEntry(index);
+ name = headerField.name;
+ } else if (index - StaticTable.length <= dynamicTable.length()) {
+ HeaderField headerField = dynamicTable.getEntry(index - StaticTable.length);
+ name = headerField.name;
+ } else {
+ throw ILLEGAL_INDEX_VALUE;
+ }
+ }
+
+ private void indexHeader(int index, HeaderListener headerListener) throws IOException {
+ if (index <= StaticTable.length) {
+ HeaderField headerField = StaticTable.getEntry(index);
+ addHeader(headerListener, headerField.name, headerField.value, false);
+ } else if (index - StaticTable.length <= dynamicTable.length()) {
+ HeaderField headerField = dynamicTable.getEntry(index - StaticTable.length);
+ addHeader(headerListener, headerField.name, headerField.value, false);
+ } else {
+ throw ILLEGAL_INDEX_VALUE;
+ }
+ }
+
+ private void insertHeader(HeaderListener headerListener, byte[] name, byte[] value,
+ IndexType indexType) {
+ addHeader(headerListener, name, value, indexType == IndexType.NEVER);
+
+ switch (indexType) {
+ case NONE:
+ case NEVER:
+ break;
+
+ case INCREMENTAL:
+ dynamicTable.add(new HeaderField(name, value));
+ break;
+
+ default:
+ throw new IllegalStateException("should not reach here");
+ }
+ }
+
+ private void addHeader(HeaderListener headerListener, byte[] name, byte[] value,
+ boolean sensitive) {
+ if (name.length == 0) {
+ throw new AssertionError("name is empty");
+ }
+ long newSize = headerSize + name.length + value.length;
+ if (newSize <= maxHeaderSize) {
+ headerListener.addHeader(name, value, sensitive);
+ headerSize = (int) newSize;
+ } else {
+ // truncation will be reported during endHeaderBlock
+ headerSize = maxHeaderSize + 1;
+ }
+ }
+
+ private boolean exceedsMaxHeaderSize(long size) {
+ // Check new header size against max header size
+ if (size + headerSize <= maxHeaderSize) {
+ return false;
+ }
+
+ // truncation will be reported during endHeaderBlock
+ headerSize = maxHeaderSize + 1;
+ return true;
+ }
+
+ private byte[] readStringLiteral(InputStream in, int length) throws IOException {
+ byte[] buf = new byte[length];
+ if (in.read(buf) != length) {
+ throw DECOMPRESSION_EXCEPTION;
+ }
+
+ if (huffmanEncoded) {
+ return Huffman.DECODER.decode(buf);
+ } else {
+ return buf;
+ }
+ }
+
+ // Unsigned Little Endian Base 128 Variable-Length Integer Encoding
+ private static int decodeULE128(InputStream in) throws IOException {
+ in.mark(5);
+ int result = 0;
+ int shift = 0;
+ while (shift < 32) {
+ if (in.available() == 0) {
+ // Buffer does not contain entire integer,
+ // reset reader index and return -1.
+ in.reset();
+ return -1;
+ }
+ byte b = (byte) in.read();
+ if (shift == 28 && (b & 0xF8) != 0) {
+ break;
+ }
+ result |= (b & 0x7F) << shift;
+ if ((b & 0x80) == 0) {
+ return result;
+ }
+ shift += 7;
+ }
+ // Value exceeds Integer.MAX_VALUE
+ in.reset();
+ throw DECOMPRESSION_EXCEPTION;
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/DynamicTable.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/DynamicTable.java
new file mode 100644
index 0000000000..2c4457992c
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/DynamicTable.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import static io.netty.handler.codec.http2.hpack.HeaderField.HEADER_ENTRY_OVERHEAD;
+
+final class DynamicTable {
+
+ // a circular queue of header fields
+ HeaderField[] headerFields;
+ int head;
+ int tail;
+ private int size;
+ private int capacity = -1; // ensure setCapacity creates the array
+
+ /**
+ * Creates a new dynamic table with the specified initial capacity.
+ */
+ DynamicTable(int initialCapacity) {
+ setCapacity(initialCapacity);
+ }
+
+ /**
+ * Return the number of header fields in the dynamic table.
+ */
+ public int length() {
+ int length;
+ if (head < tail) {
+ length = headerFields.length - tail + head;
+ } else {
+ length = head - tail;
+ }
+ return length;
+ }
+
+ /**
+ * Return the current size of the dynamic table. This is the sum of the size of the entries.
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Return the maximum allowable size of the dynamic table.
+ */
+ public int capacity() {
+ return capacity;
+ }
+
+ /**
+ * Return the header field at the given index. The first and newest entry is always at index 1,
+ * and the oldest entry is at the index length().
+ */
+ public HeaderField getEntry(int index) {
+ if (index <= 0 || index > length()) {
+ throw new IndexOutOfBoundsException();
+ }
+ int i = head - index;
+ if (i < 0) {
+ return headerFields[i + headerFields.length];
+ } else {
+ return headerFields[i];
+ }
+ }
+
+ /**
+ * Add the header field to the dynamic table. Entries are evicted from the dynamic table until
+ * the size of the table and the new header field is less than or equal to the table's capacity.
+ * If the size of the new entry is larger than the table's capacity, the dynamic table will be
+ * cleared.
+ */
+ public void add(HeaderField header) {
+ int headerSize = header.size();
+ if (headerSize > capacity) {
+ clear();
+ return;
+ }
+ while (size + headerSize > capacity) {
+ remove();
+ }
+ headerFields[head++] = header;
+ size += header.size();
+ if (head == headerFields.length) {
+ head = 0;
+ }
+ }
+
+ /**
+ * Remove and return the oldest header field from the dynamic table.
+ */
+ public HeaderField remove() {
+ HeaderField removed = headerFields[tail];
+ if (removed == null) {
+ return null;
+ }
+ size -= removed.size();
+ headerFields[tail++] = null;
+ if (tail == headerFields.length) {
+ tail = 0;
+ }
+ return removed;
+ }
+
+ /**
+ * Remove all entries from the dynamic table.
+ */
+ public void clear() {
+ while (tail != head) {
+ headerFields[tail++] = null;
+ if (tail == headerFields.length) {
+ tail = 0;
+ }
+ }
+ head = 0;
+ tail = 0;
+ size = 0;
+ }
+
+ /**
+ * Set the maximum size of the dynamic table. Entries are evicted from the dynamic table until
+ * the size of the table is less than or equal to the maximum size.
+ */
+ public void setCapacity(int capacity) {
+ if (capacity < 0) {
+ throw new IllegalArgumentException("Illegal Capacity: " + capacity);
+ }
+
+ // initially capacity will be -1 so init won't return here
+ if (this.capacity == capacity) {
+ return;
+ }
+ this.capacity = capacity;
+
+ if (capacity == 0) {
+ clear();
+ } else {
+ // initially size will be 0 so remove won't be called
+ while (size > capacity) {
+ remove();
+ }
+ }
+
+ int maxEntries = capacity / HEADER_ENTRY_OVERHEAD;
+ if (capacity % HEADER_ENTRY_OVERHEAD != 0) {
+ maxEntries++;
+ }
+
+ // check if capacity change requires us to reallocate the array
+ if (headerFields != null && headerFields.length == maxEntries) {
+ return;
+ }
+
+ HeaderField[] tmp = new HeaderField[maxEntries];
+
+ // initially length will be 0 so there will be no copy
+ int len = length();
+ int cursor = tail;
+ for (int i = 0; i < len; i++) {
+ HeaderField entry = headerFields[cursor++];
+ tmp[i] = entry;
+ if (cursor == headerFields.length) {
+ cursor = 0;
+ }
+ }
+
+ this.tail = 0;
+ this.head = tail + len;
+ this.headerFields = tmp;
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Encoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Encoder.java
new file mode 100644
index 0000000000..3c2a0c7e23
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Encoder.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import io.netty.handler.codec.http2.hpack.HpackUtil.IndexType;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+public final class Encoder {
+
+ private static final int BUCKET_SIZE = 17;
+ private static final byte[] EMPTY = {};
+
+ // for testing
+ private final boolean useIndexing;
+ private final boolean forceHuffmanOn;
+ private final boolean forceHuffmanOff;
+
+ // a linked hash map of header fields
+ private final HeaderEntry[] headerFields = new HeaderEntry[BUCKET_SIZE];
+ private final HeaderEntry head = new HeaderEntry(-1, EMPTY, EMPTY, Integer.MAX_VALUE, null);
+ private int size;
+ private int capacity;
+
+ /**
+ * Creates a new encoder.
+ */
+ public Encoder(int maxHeaderTableSize) {
+ this(maxHeaderTableSize, true, false, false);
+ }
+
+ /**
+ * Constructor for testing only.
+ */
+ Encoder(
+ int maxHeaderTableSize,
+ boolean useIndexing,
+ boolean forceHuffmanOn,
+ boolean forceHuffmanOff
+ ) {
+ if (maxHeaderTableSize < 0) {
+ throw new IllegalArgumentException("Illegal Capacity: " + maxHeaderTableSize);
+ }
+ this.useIndexing = useIndexing;
+ this.forceHuffmanOn = forceHuffmanOn;
+ this.forceHuffmanOff = forceHuffmanOff;
+ this.capacity = maxHeaderTableSize;
+ head.before = head.after = head;
+ }
+
+ /**
+ * Encode the header field into the header block.
+ */
+ public void encodeHeader(OutputStream out, byte[] name, byte[] value, boolean sensitive)
+ throws IOException {
+
+ // If the header value is sensitive then it must never be indexed
+ if (sensitive) {
+ int nameIndex = getNameIndex(name);
+ encodeLiteral(out, name, value, IndexType.NEVER, nameIndex);
+ return;
+ }
+
+ // If the peer will only use the static table
+ if (capacity == 0) {
+ int staticTableIndex = StaticTable.getIndex(name, value);
+ if (staticTableIndex == -1) {
+ int nameIndex = StaticTable.getIndex(name);
+ encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
+ } else {
+ encodeInteger(out, 0x80, 7, staticTableIndex);
+ }
+ return;
+ }
+
+ int headerSize = HeaderField.sizeOf(name, value);
+
+ // If the headerSize is greater than the max table size then it must be encoded literally
+ if (headerSize > capacity) {
+ int nameIndex = getNameIndex(name);
+ encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
+ return;
+ }
+
+ HeaderEntry headerField = getEntry(name, value);
+ if (headerField != null) {
+ int index = getIndex(headerField.index) + StaticTable.length;
+ // Section 6.1. Indexed Header Field Representation
+ encodeInteger(out, 0x80, 7, index);
+ } else {
+ int staticTableIndex = StaticTable.getIndex(name, value);
+ if (staticTableIndex != -1) {
+ // Section 6.1. Indexed Header Field Representation
+ encodeInteger(out, 0x80, 7, staticTableIndex);
+ } else {
+ int nameIndex = getNameIndex(name);
+ if (useIndexing) {
+ ensureCapacity(headerSize);
+ }
+ IndexType indexType = useIndexing ? IndexType.INCREMENTAL : IndexType.NONE;
+ encodeLiteral(out, name, value, indexType, nameIndex);
+ if (useIndexing) {
+ add(name, value);
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the maximum table size.
+ */
+ public void setMaxHeaderTableSize(OutputStream out, int maxHeaderTableSize) throws IOException {
+ if (maxHeaderTableSize < 0) {
+ throw new IllegalArgumentException("Illegal Capacity: " + maxHeaderTableSize);
+ }
+ if (capacity == maxHeaderTableSize) {
+ return;
+ }
+ capacity = maxHeaderTableSize;
+ ensureCapacity(0);
+ encodeInteger(out, 0x20, 5, maxHeaderTableSize);
+ }
+
+ /**
+ * Return the maximum table size.
+ */
+ public int getMaxHeaderTableSize() {
+ return capacity;
+ }
+
+ /**
+ * Encode integer according to Section 5.1.
+ */
+ private static void encodeInteger(OutputStream out, int mask, int n, int i) throws IOException {
+ if (n < 0 || n > 8) {
+ throw new IllegalArgumentException("N: " + n);
+ }
+ int nbits = 0xFF >>> (8 - n);
+ if (i < nbits) {
+ out.write(mask | i);
+ } else {
+ out.write(mask | nbits);
+ int length = i - nbits;
+ while (true) {
+ if ((length & ~0x7F) == 0) {
+ out.write(length);
+ return;
+ } else {
+ out.write((length & 0x7F) | 0x80);
+ length >>>= 7;
+ }
+ }
+ }
+ }
+
+ /**
+ * Encode string literal according to Section 5.2.
+ */
+ private void encodeStringLiteral(OutputStream out, byte[] string) throws IOException {
+ int huffmanLength = Huffman.ENCODER.getEncodedLength(string);
+ if ((huffmanLength < string.length && !forceHuffmanOff) || forceHuffmanOn) {
+ encodeInteger(out, 0x80, 7, huffmanLength);
+ Huffman.ENCODER.encode(out, string);
+ } else {
+ encodeInteger(out, 0x00, 7, string.length);
+ out.write(string, 0, string.length);
+ }
+ }
+
+ /**
+ * Encode literal header field according to Section 6.2.
+ */
+ private void encodeLiteral(OutputStream out, byte[] name, byte[] value, IndexType indexType,
+ int nameIndex)
+ throws IOException {
+ int mask;
+ int prefixBits;
+ switch (indexType) {
+ case INCREMENTAL:
+ mask = 0x40;
+ prefixBits = 6;
+ break;
+ case NONE:
+ mask = 0x00;
+ prefixBits = 4;
+ break;
+ case NEVER:
+ mask = 0x10;
+ prefixBits = 4;
+ break;
+ default:
+ throw new IllegalStateException("should not reach here");
+ }
+ encodeInteger(out, mask, prefixBits, nameIndex == -1 ? 0 : nameIndex);
+ if (nameIndex == -1) {
+ encodeStringLiteral(out, name);
+ }
+ encodeStringLiteral(out, value);
+ }
+
+ private int getNameIndex(byte[] name) {
+ int index = StaticTable.getIndex(name);
+ if (index == -1) {
+ index = getIndex(name);
+ if (index >= 0) {
+ index += StaticTable.length;
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Ensure that the dynamic table has enough room to hold 'headerSize' more bytes. Removes the
+ * oldest entry from the dynamic table until sufficient space is available.
+ */
+ private void ensureCapacity(int headerSize) throws IOException {
+ while (size + headerSize > capacity) {
+ int index = length();
+ if (index == 0) {
+ break;
+ }
+ remove();
+ }
+ }
+
+ /**
+ * Return the number of header fields in the dynamic table. Exposed for testing.
+ */
+ int length() {
+ return size == 0 ? 0 : head.after.index - head.before.index + 1;
+ }
+
+ /**
+ * Return the size of the dynamic table. Exposed for testing.
+ */
+ int size() {
+ return size;
+ }
+
+ /**
+ * Return the header field at the given index. Exposed for testing.
+ */
+ HeaderField getHeaderField(int index) {
+ HeaderEntry entry = head;
+ while (index-- >= 0) {
+ entry = entry.before;
+ }
+ return entry;
+ }
+
+ /**
+ * Returns the header entry with the lowest index value for the header field. Returns null if
+ * header field is not in the dynamic table.
+ */
+ private HeaderEntry getEntry(byte[] name, byte[] value) {
+ if (length() == 0 || name == null || value == null) {
+ return null;
+ }
+ int h = hash(name);
+ int i = index(h);
+ for (HeaderEntry e = headerFields[i]; e != null; e = e.next) {
+ if (e.hash == h &&
+ HpackUtil.equals(name, e.name) &&
+ HpackUtil.equals(value, e.value)) {
+ return e;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the lowest index value for the header field name in the dynamic table. Returns -1 if
+ * the header field name is not in the dynamic table.
+ */
+ private int getIndex(byte[] name) {
+ if (length() == 0 || name == null) {
+ return -1;
+ }
+ int h = hash(name);
+ int i = index(h);
+ int index = -1;
+ for (HeaderEntry e = headerFields[i]; e != null; e = e.next) {
+ if (e.hash == h && HpackUtil.equals(name, e.name)) {
+ index = e.index;
+ break;
+ }
+ }
+ return getIndex(index);
+ }
+
+ /**
+ * Compute the index into the dynamic table given the index in the header entry.
+ */
+ private int getIndex(int index) {
+ if (index == -1) {
+ return index;
+ }
+ return index - head.before.index + 1;
+ }
+
+ /**
+ * Add the header field to the dynamic table. Entries are evicted from the dynamic table until
+ * the size of the table and the new header field is less than the table's capacity. If the size
+ * of the new entry is larger than the table's capacity, the dynamic table will be cleared.
+ */
+ private void add(byte[] name, byte[] value) {
+ int headerSize = HeaderField.sizeOf(name, value);
+
+ // Clear the table if the header field size is larger than the capacity.
+ if (headerSize > capacity) {
+ clear();
+ return;
+ }
+
+ // Evict oldest entries until we have enough capacity.
+ while (size + headerSize > capacity) {
+ remove();
+ }
+
+ // Copy name and value that modifications of original do not affect the dynamic table.
+ name = Arrays.copyOf(name, name.length);
+ value = Arrays.copyOf(value, value.length);
+
+ int h = hash(name);
+ int i = index(h);
+ HeaderEntry old = headerFields[i];
+ HeaderEntry e = new HeaderEntry(h, name, value, head.before.index - 1, old);
+ headerFields[i] = e;
+ e.addBefore(head);
+ size += headerSize;
+ }
+
+ /**
+ * Remove and return the oldest header field from the dynamic table.
+ */
+ private HeaderField remove() {
+ if (size == 0) {
+ return null;
+ }
+ HeaderEntry eldest = head.after;
+ int h = eldest.hash;
+ int i = index(h);
+ HeaderEntry prev = headerFields[i];
+ HeaderEntry e = prev;
+ while (e != null) {
+ HeaderEntry next = e.next;
+ if (e == eldest) {
+ if (prev == eldest) {
+ headerFields[i] = next;
+ } else {
+ prev.next = next;
+ }
+ eldest.remove();
+ size -= eldest.size();
+ return eldest;
+ }
+ prev = e;
+ e = next;
+ }
+ return null;
+ }
+
+ /**
+ * Remove all entries from the dynamic table.
+ */
+ private void clear() {
+ Arrays.fill(headerFields, null);
+ head.before = head.after = head;
+ this.size = 0;
+ }
+
+ /**
+ * Returns the hash code for the given header field name.
+ */
+ private static int hash(byte[] name) {
+ int h = 0;
+ for (int i = 0; i < name.length; i++) {
+ h = 31 * h + name[i];
+ }
+ if (h > 0) {
+ return h;
+ } else if (h == Integer.MIN_VALUE) {
+ return Integer.MAX_VALUE;
+ } else {
+ return -h;
+ }
+ }
+
+ /**
+ * Returns the index into the hash table for the hash code h.
+ */
+ private static int index(int h) {
+ return h % BUCKET_SIZE;
+ }
+
+ /**
+ * A linked hash map HeaderField entry.
+ */
+ private static class HeaderEntry extends HeaderField {
+ // These fields comprise the doubly linked list used for iteration.
+ HeaderEntry before, after;
+
+ // These fields comprise the chained list for header fields with the same hash.
+ HeaderEntry next;
+ int hash;
+
+ // This is used to compute the index in the dynamic table.
+ int index;
+
+ /**
+ * Creates new entry.
+ */
+ HeaderEntry(int hash, byte[] name, byte[] value, int index, HeaderEntry next) {
+ super(name, value);
+ this.index = index;
+ this.hash = hash;
+ this.next = next;
+ }
+
+ /**
+ * Removes this entry from the linked list.
+ */
+ private void remove() {
+ before.after = after;
+ after.before = before;
+ }
+
+ /**
+ * Inserts this entry before the specified existing entry in the list.
+ */
+ private void addBefore(HeaderEntry existingEntry) {
+ after = existingEntry;
+ before = existingEntry.before;
+ before.after = this;
+ after.before = this;
+ }
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderField.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderField.java
new file mode 100644
index 0000000000..4a028658e5
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderField.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import static io.netty.handler.codec.http2.hpack.HpackUtil.ISO_8859_1;
+import static io.netty.handler.codec.http2.hpack.HpackUtil.requireNonNull;
+
+class HeaderField implements Comparable {
+
+ // Section 4.1. Calculating Table Size
+ // The additional 32 octets account for an estimated
+ // overhead associated with the structure.
+ static final int HEADER_ENTRY_OVERHEAD = 32;
+
+ static int sizeOf(byte[] name, byte[] value) {
+ return name.length + value.length + HEADER_ENTRY_OVERHEAD;
+ }
+
+ final byte[] name;
+ final byte[] value;
+
+ // This constructor can only be used if name and value are ISO-8859-1 encoded.
+ HeaderField(String name, String value) {
+ this(name.getBytes(ISO_8859_1), value.getBytes(ISO_8859_1));
+ }
+
+ HeaderField(byte[] name, byte[] value) {
+ this.name = requireNonNull(name);
+ this.value = requireNonNull(value);
+ }
+
+ int size() {
+ return name.length + value.length + HEADER_ENTRY_OVERHEAD;
+ }
+
+ @Override
+ public int hashCode() {
+ // TODO(nmittler): Netty's build rules require this. Probably need a better implementation.
+ return super.hashCode();
+ }
+
+ @Override
+ public int compareTo(HeaderField anotherHeaderField) {
+ int ret = compareTo(name, anotherHeaderField.name);
+ if (ret == 0) {
+ ret = compareTo(value, anotherHeaderField.value);
+ }
+ return ret;
+ }
+
+ private int compareTo(byte[] s1, byte[] s2) {
+ int len1 = s1.length;
+ int len2 = s2.length;
+ int lim = Math.min(len1, len2);
+
+ int k = 0;
+ while (k < lim) {
+ byte b1 = s1[k];
+ byte b2 = s2[k];
+ if (b1 != b2) {
+ return b1 - b2;
+ }
+ k++;
+ }
+ return len1 - len2;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof HeaderField)) {
+ return false;
+ }
+ HeaderField other = (HeaderField) obj;
+ boolean nameEquals = HpackUtil.equals(name, other.name);
+ boolean valueEquals = HpackUtil.equals(value, other.value);
+ return nameEquals && valueEquals;
+ }
+
+ @Override
+ public String toString() {
+ String nameString = new String(name);
+ String valueString = new String(value);
+ return nameString + ": " + valueString;
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderListener.java
new file mode 100644
index 0000000000..6fd488d41e
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HeaderListener.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+public interface HeaderListener {
+
+ /**
+ * emitHeader is called by the decoder during header field emission.
+ * The name and value byte arrays must not be modified.
+ */
+ void addHeader(byte[] name, byte[] value, boolean sensitive);
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HpackUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HpackUtil.java
new file mode 100644
index 0000000000..53660f6657
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HpackUtil.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import java.nio.charset.Charset;
+
+final class HpackUtil {
+
+ static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+
+ /**
+ * A string compare that doesn't leak timing information.
+ */
+ static boolean equals(byte[] s1, byte[] s2) {
+ if (s1.length != s2.length) {
+ return false;
+ }
+ char c = 0;
+ for (int i = 0; i < s1.length; i++) {
+ c |= s1[i] ^ s2[i];
+ }
+ return c == 0;
+ }
+
+ /**
+ * Checks that the specified object reference is not {@code null}.
+ */
+ static T requireNonNull(T obj) {
+ if (obj == null) {
+ throw new NullPointerException();
+ }
+ return obj;
+ }
+
+ // Section 6.2. Literal Header Field Representation
+ enum IndexType {
+ INCREMENTAL, // Section 6.2.1. Literal Header Field with Incremental Indexing
+ NONE, // Section 6.2.2. Literal Header Field without Indexing
+ NEVER // Section 6.2.3. Literal Header Field never Indexed
+ }
+
+ // Appendix B: Huffman Codes
+ // http://tools.ietf.org/html/rfc7541#appendix-B
+ static final int[] HUFFMAN_CODES = {
+ 0x1ff8,
+ 0x7fffd8,
+ 0xfffffe2,
+ 0xfffffe3,
+ 0xfffffe4,
+ 0xfffffe5,
+ 0xfffffe6,
+ 0xfffffe7,
+ 0xfffffe8,
+ 0xffffea,
+ 0x3ffffffc,
+ 0xfffffe9,
+ 0xfffffea,
+ 0x3ffffffd,
+ 0xfffffeb,
+ 0xfffffec,
+ 0xfffffed,
+ 0xfffffee,
+ 0xfffffef,
+ 0xffffff0,
+ 0xffffff1,
+ 0xffffff2,
+ 0x3ffffffe,
+ 0xffffff3,
+ 0xffffff4,
+ 0xffffff5,
+ 0xffffff6,
+ 0xffffff7,
+ 0xffffff8,
+ 0xffffff9,
+ 0xffffffa,
+ 0xffffffb,
+ 0x14,
+ 0x3f8,
+ 0x3f9,
+ 0xffa,
+ 0x1ff9,
+ 0x15,
+ 0xf8,
+ 0x7fa,
+ 0x3fa,
+ 0x3fb,
+ 0xf9,
+ 0x7fb,
+ 0xfa,
+ 0x16,
+ 0x17,
+ 0x18,
+ 0x0,
+ 0x1,
+ 0x2,
+ 0x19,
+ 0x1a,
+ 0x1b,
+ 0x1c,
+ 0x1d,
+ 0x1e,
+ 0x1f,
+ 0x5c,
+ 0xfb,
+ 0x7ffc,
+ 0x20,
+ 0xffb,
+ 0x3fc,
+ 0x1ffa,
+ 0x21,
+ 0x5d,
+ 0x5e,
+ 0x5f,
+ 0x60,
+ 0x61,
+ 0x62,
+ 0x63,
+ 0x64,
+ 0x65,
+ 0x66,
+ 0x67,
+ 0x68,
+ 0x69,
+ 0x6a,
+ 0x6b,
+ 0x6c,
+ 0x6d,
+ 0x6e,
+ 0x6f,
+ 0x70,
+ 0x71,
+ 0x72,
+ 0xfc,
+ 0x73,
+ 0xfd,
+ 0x1ffb,
+ 0x7fff0,
+ 0x1ffc,
+ 0x3ffc,
+ 0x22,
+ 0x7ffd,
+ 0x3,
+ 0x23,
+ 0x4,
+ 0x24,
+ 0x5,
+ 0x25,
+ 0x26,
+ 0x27,
+ 0x6,
+ 0x74,
+ 0x75,
+ 0x28,
+ 0x29,
+ 0x2a,
+ 0x7,
+ 0x2b,
+ 0x76,
+ 0x2c,
+ 0x8,
+ 0x9,
+ 0x2d,
+ 0x77,
+ 0x78,
+ 0x79,
+ 0x7a,
+ 0x7b,
+ 0x7ffe,
+ 0x7fc,
+ 0x3ffd,
+ 0x1ffd,
+ 0xffffffc,
+ 0xfffe6,
+ 0x3fffd2,
+ 0xfffe7,
+ 0xfffe8,
+ 0x3fffd3,
+ 0x3fffd4,
+ 0x3fffd5,
+ 0x7fffd9,
+ 0x3fffd6,
+ 0x7fffda,
+ 0x7fffdb,
+ 0x7fffdc,
+ 0x7fffdd,
+ 0x7fffde,
+ 0xffffeb,
+ 0x7fffdf,
+ 0xffffec,
+ 0xffffed,
+ 0x3fffd7,
+ 0x7fffe0,
+ 0xffffee,
+ 0x7fffe1,
+ 0x7fffe2,
+ 0x7fffe3,
+ 0x7fffe4,
+ 0x1fffdc,
+ 0x3fffd8,
+ 0x7fffe5,
+ 0x3fffd9,
+ 0x7fffe6,
+ 0x7fffe7,
+ 0xffffef,
+ 0x3fffda,
+ 0x1fffdd,
+ 0xfffe9,
+ 0x3fffdb,
+ 0x3fffdc,
+ 0x7fffe8,
+ 0x7fffe9,
+ 0x1fffde,
+ 0x7fffea,
+ 0x3fffdd,
+ 0x3fffde,
+ 0xfffff0,
+ 0x1fffdf,
+ 0x3fffdf,
+ 0x7fffeb,
+ 0x7fffec,
+ 0x1fffe0,
+ 0x1fffe1,
+ 0x3fffe0,
+ 0x1fffe2,
+ 0x7fffed,
+ 0x3fffe1,
+ 0x7fffee,
+ 0x7fffef,
+ 0xfffea,
+ 0x3fffe2,
+ 0x3fffe3,
+ 0x3fffe4,
+ 0x7ffff0,
+ 0x3fffe5,
+ 0x3fffe6,
+ 0x7ffff1,
+ 0x3ffffe0,
+ 0x3ffffe1,
+ 0xfffeb,
+ 0x7fff1,
+ 0x3fffe7,
+ 0x7ffff2,
+ 0x3fffe8,
+ 0x1ffffec,
+ 0x3ffffe2,
+ 0x3ffffe3,
+ 0x3ffffe4,
+ 0x7ffffde,
+ 0x7ffffdf,
+ 0x3ffffe5,
+ 0xfffff1,
+ 0x1ffffed,
+ 0x7fff2,
+ 0x1fffe3,
+ 0x3ffffe6,
+ 0x7ffffe0,
+ 0x7ffffe1,
+ 0x3ffffe7,
+ 0x7ffffe2,
+ 0xfffff2,
+ 0x1fffe4,
+ 0x1fffe5,
+ 0x3ffffe8,
+ 0x3ffffe9,
+ 0xffffffd,
+ 0x7ffffe3,
+ 0x7ffffe4,
+ 0x7ffffe5,
+ 0xfffec,
+ 0xfffff3,
+ 0xfffed,
+ 0x1fffe6,
+ 0x3fffe9,
+ 0x1fffe7,
+ 0x1fffe8,
+ 0x7ffff3,
+ 0x3fffea,
+ 0x3fffeb,
+ 0x1ffffee,
+ 0x1ffffef,
+ 0xfffff4,
+ 0xfffff5,
+ 0x3ffffea,
+ 0x7ffff4,
+ 0x3ffffeb,
+ 0x7ffffe6,
+ 0x3ffffec,
+ 0x3ffffed,
+ 0x7ffffe7,
+ 0x7ffffe8,
+ 0x7ffffe9,
+ 0x7ffffea,
+ 0x7ffffeb,
+ 0xffffffe,
+ 0x7ffffec,
+ 0x7ffffed,
+ 0x7ffffee,
+ 0x7ffffef,
+ 0x7fffff0,
+ 0x3ffffee,
+ 0x3fffffff // EOS
+ };
+
+ static final byte[] HUFFMAN_CODE_LENGTHS = {
+ 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28,
+ 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+ 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6,
+ 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10,
+ 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6,
+ 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5,
+ 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28,
+ 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23,
+ 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24,
+ 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23,
+ 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23,
+ 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25,
+ 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27,
+ 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23,
+ 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26,
+ 30 // EOS
+ };
+
+ static final int HUFFMAN_EOS = 256;
+
+ private HpackUtil() {
+ // utility class
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Huffman.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Huffman.java
new file mode 100644
index 0000000000..771fab8e3e
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/Huffman.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import static io.netty.handler.codec.http2.hpack.HpackUtil.HUFFMAN_CODES;
+import static io.netty.handler.codec.http2.hpack.HpackUtil.HUFFMAN_CODE_LENGTHS;
+
+public final class Huffman {
+
+ /**
+ * Huffman Decoder
+ */
+ public static final HuffmanDecoder DECODER =
+ new HuffmanDecoder(HUFFMAN_CODES, HUFFMAN_CODE_LENGTHS);
+
+ /**
+ * Huffman Encoder
+ */
+ public static final HuffmanEncoder ENCODER =
+ new HuffmanEncoder(HUFFMAN_CODES, HUFFMAN_CODE_LENGTHS);
+
+ private Huffman() {
+ // utility class
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanDecoder.java
new file mode 100644
index 0000000000..c0b33e05d7
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanDecoder.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+final class HuffmanDecoder {
+
+ private static final IOException EOS_DECODED = new IOException("EOS Decoded");
+ private static final IOException INVALID_PADDING = new IOException("Invalid Padding");
+
+ private final Node root;
+
+ /**
+ * Creates a new Huffman decoder with the specified Huffman coding.
+ *
+ * @param codes the Huffman codes indexed by symbol
+ * @param lengths the length of each Huffman code
+ */
+ HuffmanDecoder(int[] codes, byte[] lengths) {
+ if (codes.length != 257 || codes.length != lengths.length) {
+ throw new IllegalArgumentException("invalid Huffman coding");
+ }
+ root = buildTree(codes, lengths);
+ }
+
+ /**
+ * Decompresses the given Huffman coded string literal.
+ *
+ * @param buf the string literal to be decoded
+ * @return the output stream for the compressed data
+ * @throws IOException if an I/O error occurs. In particular, an IOException may be
+ * thrown if the output stream has been closed.
+ */
+ public byte[] decode(byte[] buf) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ Node node = root;
+ int current = 0;
+ int bits = 0;
+ for (int i = 0; i < buf.length; i++) {
+ int b = buf[i] & 0xFF;
+ current = (current << 8) | b;
+ bits += 8;
+ while (bits >= 8) {
+ int c = (current >>> (bits - 8)) & 0xFF;
+ node = node.children[c];
+ bits -= node.bits;
+ if (node.isTerminal()) {
+ if (node.symbol == HpackUtil.HUFFMAN_EOS) {
+ throw EOS_DECODED;
+ }
+ baos.write(node.symbol);
+ node = root;
+ }
+ }
+ }
+
+ while (bits > 0) {
+ int c = (current << (8 - bits)) & 0xFF;
+ node = node.children[c];
+ if (node.isTerminal() && node.bits <= bits) {
+ bits -= node.bits;
+ baos.write(node.symbol);
+ node = root;
+ } else {
+ break;
+ }
+ }
+
+ // Section 5.2. String Literal Representation
+ // Padding not corresponding to the most significant bits of the code
+ // for the EOS symbol (0xFF) MUST be treated as a decoding error.
+ int mask = (1 << bits) - 1;
+ if ((current & mask) != mask) {
+ throw INVALID_PADDING;
+ }
+
+ return baos.toByteArray();
+ }
+
+ private static final class Node {
+
+ private final int symbol; // terminal nodes have a symbol
+ private final int bits; // number of bits matched by the node
+ private final Node[] children; // internal nodes have children
+
+ /**
+ * Construct an internal node
+ */
+ private Node() {
+ symbol = 0;
+ bits = 8;
+ children = new Node[256];
+ }
+
+ /**
+ * Construct a terminal node
+ *
+ * @param symbol the symbol the node represents
+ * @param bits the number of bits matched by this node
+ */
+ private Node(int symbol, int bits) {
+ assert bits > 0 && bits <= 8;
+ this.symbol = symbol;
+ this.bits = bits;
+ children = null;
+ }
+
+ private boolean isTerminal() {
+ return children == null;
+ }
+ }
+
+ private static Node buildTree(int[] codes, byte[] lengths) {
+ Node root = new Node();
+ for (int i = 0; i < codes.length; i++) {
+ insert(root, i, codes[i], lengths[i]);
+ }
+ return root;
+ }
+
+ private static void insert(Node root, int symbol, int code, byte length) {
+ // traverse tree using the most significant bytes of code
+ Node current = root;
+ while (length > 8) {
+ if (current.isTerminal()) {
+ throw new IllegalStateException("invalid Huffman code: prefix not unique");
+ }
+ length -= 8;
+ int i = (code >>> length) & 0xFF;
+ if (current.children[i] == null) {
+ current.children[i] = new Node();
+ }
+ current = current.children[i];
+ }
+
+ Node terminal = new Node(symbol, length);
+ int shift = 8 - length;
+ int start = (code << shift) & 0xFF;
+ int end = 1 << shift;
+ for (int i = start; i < start + end; i++) {
+ current.children[i] = terminal;
+ }
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanEncoder.java
new file mode 100644
index 0000000000..1e7e2654ba
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/HuffmanEncoder.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+final class HuffmanEncoder {
+
+ private final int[] codes;
+ private final byte[] lengths;
+
+ /**
+ * Creates a new Huffman encoder with the specified Huffman coding.
+ *
+ * @param codes the Huffman codes indexed by symbol
+ * @param lengths the length of each Huffman code
+ */
+ HuffmanEncoder(int[] codes, byte[] lengths) {
+ this.codes = codes;
+ this.lengths = lengths;
+ }
+
+ /**
+ * Compresses the input string literal using the Huffman coding.
+ *
+ * @param out the output stream for the compressed data
+ * @param data the string literal to be Huffman encoded
+ * @throws IOException if an I/O error occurs.
+ * @see HuffmanEncoder#encode(OutputStream, byte[], int, int)
+ */
+ public void encode(OutputStream out, byte[] data) throws IOException {
+ encode(out, data, 0, data.length);
+ }
+
+ /**
+ * Compresses the input string literal using the Huffman coding.
+ *
+ * @param out the output stream for the compressed data
+ * @param data the string literal to be Huffman encoded
+ * @param off the start offset in the data
+ * @param len the number of bytes to encode
+ * @throws IOException if an I/O error occurs. In particular, an IOException may be
+ * thrown if the output stream has been closed.
+ */
+ public void encode(OutputStream out, byte[] data, int off, int len) throws IOException {
+ if (out == null) {
+ throw new NullPointerException("out");
+ } else if (data == null) {
+ throw new NullPointerException("data");
+ } else if (off < 0 || len < 0 || (off + len) < 0 || off > data.length ||
+ (off + len) > data.length) {
+ throw new IndexOutOfBoundsException();
+ } else if (len == 0) {
+ return;
+ }
+
+ long current = 0;
+ int n = 0;
+
+ for (int i = 0; i < len; i++) {
+ int b = data[off + i] & 0xFF;
+ int code = codes[b];
+ int nbits = lengths[b];
+
+ current <<= nbits;
+ current |= code;
+ n += nbits;
+
+ while (n >= 8) {
+ n -= 8;
+ out.write((int) (current >> n));
+ }
+ }
+
+ if (n > 0) {
+ current <<= 8 - n;
+ current |= 0xFF >>> n; // this should be EOS symbol
+ out.write((int) current);
+ }
+ }
+
+ /**
+ * Returns the number of bytes required to Huffman encode the input string literal.
+ *
+ * @param data the string literal to be Huffman encoded
+ * @return the number of bytes required to Huffman encode data
+ */
+ public int getEncodedLength(byte[] data) {
+ if (data == null) {
+ throw new NullPointerException("data");
+ }
+ long len = 0;
+ for (byte b : data) {
+ len += lengths[b & 0xFF];
+ }
+ return (int) ((len + 7) >> 3);
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/StaticTable.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/StaticTable.java
new file mode 100644
index 0000000000..ff8543b324
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/StaticTable.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+final class StaticTable {
+
+ private static final String EMPTY = "";
+
+ // Appendix A: Static Table
+ // http://tools.ietf.org/html/rfc7541#appendix-A
+ private static final List STATIC_TABLE = Arrays.asList(
+ /* 1 */ new HeaderField(":authority", EMPTY),
+ /* 2 */ new HeaderField(":method", "GET"),
+ /* 3 */ new HeaderField(":method", "POST"),
+ /* 4 */ new HeaderField(":path", "/"),
+ /* 5 */ new HeaderField(":path", "/index.html"),
+ /* 6 */ new HeaderField(":scheme", "http"),
+ /* 7 */ new HeaderField(":scheme", "https"),
+ /* 8 */ new HeaderField(":status", "200"),
+ /* 9 */ new HeaderField(":status", "204"),
+ /* 10 */ new HeaderField(":status", "206"),
+ /* 11 */ new HeaderField(":status", "304"),
+ /* 12 */ new HeaderField(":status", "400"),
+ /* 13 */ new HeaderField(":status", "404"),
+ /* 14 */ new HeaderField(":status", "500"),
+ /* 15 */ new HeaderField("accept-charset", EMPTY),
+ /* 16 */ new HeaderField("accept-encoding", "gzip, deflate"),
+ /* 17 */ new HeaderField("accept-language", EMPTY),
+ /* 18 */ new HeaderField("accept-ranges", EMPTY),
+ /* 19 */ new HeaderField("accept", EMPTY),
+ /* 20 */ new HeaderField("access-control-allow-origin", EMPTY),
+ /* 21 */ new HeaderField("age", EMPTY),
+ /* 22 */ new HeaderField("allow", EMPTY),
+ /* 23 */ new HeaderField("authorization", EMPTY),
+ /* 24 */ new HeaderField("cache-control", EMPTY),
+ /* 25 */ new HeaderField("content-disposition", EMPTY),
+ /* 26 */ new HeaderField("content-encoding", EMPTY),
+ /* 27 */ new HeaderField("content-language", EMPTY),
+ /* 28 */ new HeaderField("content-length", EMPTY),
+ /* 29 */ new HeaderField("content-location", EMPTY),
+ /* 30 */ new HeaderField("content-range", EMPTY),
+ /* 31 */ new HeaderField("content-type", EMPTY),
+ /* 32 */ new HeaderField("cookie", EMPTY),
+ /* 33 */ new HeaderField("date", EMPTY),
+ /* 34 */ new HeaderField("etag", EMPTY),
+ /* 35 */ new HeaderField("expect", EMPTY),
+ /* 36 */ new HeaderField("expires", EMPTY),
+ /* 37 */ new HeaderField("from", EMPTY),
+ /* 38 */ new HeaderField("host", EMPTY),
+ /* 39 */ new HeaderField("if-match", EMPTY),
+ /* 40 */ new HeaderField("if-modified-since", EMPTY),
+ /* 41 */ new HeaderField("if-none-match", EMPTY),
+ /* 42 */ new HeaderField("if-range", EMPTY),
+ /* 43 */ new HeaderField("if-unmodified-since", EMPTY),
+ /* 44 */ new HeaderField("last-modified", EMPTY),
+ /* 45 */ new HeaderField("link", EMPTY),
+ /* 46 */ new HeaderField("location", EMPTY),
+ /* 47 */ new HeaderField("max-forwards", EMPTY),
+ /* 48 */ new HeaderField("proxy-authenticate", EMPTY),
+ /* 49 */ new HeaderField("proxy-authorization", EMPTY),
+ /* 50 */ new HeaderField("range", EMPTY),
+ /* 51 */ new HeaderField("referer", EMPTY),
+ /* 52 */ new HeaderField("refresh", EMPTY),
+ /* 53 */ new HeaderField("retry-after", EMPTY),
+ /* 54 */ new HeaderField("server", EMPTY),
+ /* 55 */ new HeaderField("set-cookie", EMPTY),
+ /* 56 */ new HeaderField("strict-transport-security", EMPTY),
+ /* 57 */ new HeaderField("transfer-encoding", EMPTY),
+ /* 58 */ new HeaderField("user-agent", EMPTY),
+ /* 59 */ new HeaderField("vary", EMPTY),
+ /* 60 */ new HeaderField("via", EMPTY),
+ /* 61 */ new HeaderField("www-authenticate", EMPTY)
+ );
+
+ private static final Map STATIC_INDEX_BY_NAME = createMap();
+
+ /**
+ * The number of header fields in the static table.
+ */
+ static final int length = STATIC_TABLE.size();
+
+ /**
+ * Return the header field at the given index value.
+ */
+ static HeaderField getEntry(int index) {
+ return STATIC_TABLE.get(index - 1);
+ }
+
+ /**
+ * Returns the lowest index value for the given header field name in the static table. Returns
+ * -1 if the header field name is not in the static table.
+ */
+ static int getIndex(byte[] name) {
+ String nameString = new String(name, 0, name.length, HpackUtil.ISO_8859_1);
+ Integer index = STATIC_INDEX_BY_NAME.get(nameString);
+ if (index == null) {
+ return -1;
+ }
+ return index;
+ }
+
+ /**
+ * Returns the index value for the given header field in the static table. Returns -1 if the
+ * header field is not in the static table.
+ */
+ static int getIndex(byte[] name, byte[] value) {
+ int index = getIndex(name);
+ if (index == -1) {
+ return -1;
+ }
+
+ // Note this assumes all entries for a given header field are sequential.
+ while (index <= length) {
+ HeaderField entry = getEntry(index);
+ if (!HpackUtil.equals(name, entry.name)) {
+ break;
+ }
+ if (HpackUtil.equals(value, entry.value)) {
+ return index;
+ }
+ index++;
+ }
+
+ return -1;
+ }
+
+ // create a map of header name to index value to allow quick lookup
+ private static Map createMap() {
+ int length = STATIC_TABLE.size();
+ HashMap ret = new HashMap(length);
+ // Iterate through the static table in reverse order to
+ // save the smallest index for a given name in the map.
+ for (int index = length; index > 0; index--) {
+ HeaderField entry = getEntry(index);
+ String name = new String(entry.name, 0, entry.name.length, HpackUtil.ISO_8859_1);
+ ret.put(name, index);
+ }
+ return ret;
+ }
+
+ // singleton
+ private StaticTable() {
+ }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/package-info.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/package-info.java
new file mode 100644
index 0000000000..e56f490857
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/hpack/package-info.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.
+ */
+
+/**
+ * HPACK: Header Compression for HTTP/2
+ */
+package io.netty.handler.codec.http2.hpack;
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java
index fd12cfe163..dbcf25d469 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java
@@ -15,9 +15,9 @@
package io.netty.handler.codec.http2;
-import com.twitter.hpack.Encoder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http2.hpack.Encoder;
import io.netty.util.AsciiString;
import org.junit.Before;
import org.junit.Test;
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/DecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/DecoderTest.java
new file mode 100644
index 0000000000..4a3d60b535
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/DecoderTest.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import static io.netty.handler.codec.http2.hpack.HpackUtil.ISO_8859_1;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+public class DecoderTest {
+
+ private static final int MAX_HEADER_SIZE = 8192;
+ private static final int MAX_HEADER_TABLE_SIZE = 4096;
+
+ private Decoder decoder;
+ private HeaderListener mockListener;
+
+ private static String hex(String s) {
+ return Hex.encodeHexString(s.getBytes());
+ }
+
+ private static byte[] getBytes(String s) {
+ return s.getBytes(ISO_8859_1);
+ }
+
+ private void decode(String encoded) throws IOException {
+ byte[] b = Hex.decodeHex(encoded.toCharArray());
+ decoder.decode(new ByteArrayInputStream(b), mockListener);
+ }
+
+ @Before
+ public void setUp() {
+ decoder = new Decoder(MAX_HEADER_SIZE, MAX_HEADER_TABLE_SIZE);
+ mockListener = mock(HeaderListener.class);
+ }
+
+ @Test
+ public void testIncompleteIndex() throws IOException {
+ // Verify incomplete indices are unread
+ byte[] compressed = Hex.decodeHex("FFF0".toCharArray());
+ ByteArrayInputStream in = new ByteArrayInputStream(compressed);
+ decoder.decode(in, mockListener);
+ assertEquals(1, in.available());
+ decoder.decode(in, mockListener);
+ assertEquals(1, in.available());
+ }
+
+ @Test(expected = IOException.class)
+ public void testUnusedIndex() throws IOException {
+ // Index 0 is not used
+ decode("80");
+ }
+
+ @Test(expected = IOException.class)
+ public void testIllegalIndex() throws IOException {
+ // Index larger than the header table
+ decode("FF00");
+ }
+
+ @Test(expected = IOException.class)
+ public void testInsidiousIndex() throws IOException {
+ // Insidious index so the last shift causes sign overflow
+ decode("FF8080808008");
+ }
+
+ @Test
+ public void testDynamicTableSizeUpdate() throws Exception {
+ decode("20");
+ assertEquals(0, decoder.getMaxHeaderTableSize());
+ decode("3FE11F");
+ assertEquals(4096, decoder.getMaxHeaderTableSize());
+ }
+
+ @Test
+ public void testDynamicTableSizeUpdateRequired() throws Exception {
+ decoder.setMaxHeaderTableSize(32);
+ decode("3F00");
+ assertEquals(31, decoder.getMaxHeaderTableSize());
+ }
+
+ @Test(expected = IOException.class)
+ public void testIllegalDynamicTableSizeUpdate() throws Exception {
+ // max header table size = MAX_HEADER_TABLE_SIZE + 1
+ decode("3FE21F");
+ }
+
+ @Test(expected = IOException.class)
+ public void testInsidiousMaxDynamicTableSize() throws IOException {
+ // max header table size sign overflow
+ decode("3FE1FFFFFF07");
+ }
+
+ @Test
+ public void testReduceMaxDynamicTableSize() throws Exception {
+ decoder.setMaxHeaderTableSize(0);
+ assertEquals(0, decoder.getMaxHeaderTableSize());
+ decode("2081");
+ }
+
+ @Test(expected = IOException.class)
+ public void testTooLargeDynamicTableSizeUpdate() throws Exception {
+ decoder.setMaxHeaderTableSize(0);
+ assertEquals(0, decoder.getMaxHeaderTableSize());
+ decode("21"); // encoder max header table size not small enough
+ }
+
+ @Test(expected = IOException.class)
+ public void testMissingDynamicTableSizeUpdate() throws Exception {
+ decoder.setMaxHeaderTableSize(0);
+ assertEquals(0, decoder.getMaxHeaderTableSize());
+ decode("81");
+ }
+
+ @Test(expected = IOException.class)
+ public void testLiteralWithIncrementalIndexingWithEmptyName() throws Exception {
+ decode("000005" + hex("value"));
+ }
+
+ @Test
+ public void testLiteralWithIncrementalIndexingCompleteEviction() throws Exception {
+ // Verify indexed host header
+ decode("4004" + hex("name") + "05" + hex("value"));
+ verify(mockListener).addHeader(getBytes("name"), getBytes("value"), false);
+ verifyNoMoreInteractions(mockListener);
+ assertFalse(decoder.endHeaderBlock());
+
+ reset(mockListener);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 4096; i++) {
+ sb.append("a");
+ }
+ String value = sb.toString();
+ sb = new StringBuilder();
+ sb.append("417F811F");
+ for (int i = 0; i < 4096; i++) {
+ sb.append("61"); // 'a'
+ }
+ decode(sb.toString());
+ verify(mockListener).addHeader(getBytes(":authority"), getBytes(value), false);
+ verifyNoMoreInteractions(mockListener);
+ assertFalse(decoder.endHeaderBlock());
+
+ // Verify next header is inserted at index 62
+ decode("4004" + hex("name") + "05" + hex("value") + "BE");
+ verify(mockListener, times(2)).addHeader(getBytes("name"), getBytes("value"), false);
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void testLiteralWithIncrementalIndexingWithLargeName() throws Exception {
+ // Ignore header name that exceeds max header size
+ StringBuilder sb = new StringBuilder();
+ sb.append("407F817F");
+ for (int i = 0; i < 16384; i++) {
+ sb.append("61"); // 'a'
+ }
+ sb.append("00");
+ decode(sb.toString());
+ verifyNoMoreInteractions(mockListener);
+
+ // Verify header block is reported as truncated
+ assertTrue(decoder.endHeaderBlock());
+
+ // Verify next header is inserted at index 62
+ decode("4004" + hex("name") + "05" + hex("value") + "BE");
+ verify(mockListener, times(2)).addHeader(getBytes("name"), getBytes("value"), false);
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test
+ public void testLiteralWithIncrementalIndexingWithLargeValue() throws Exception {
+ // Ignore header that exceeds max header size
+ StringBuilder sb = new StringBuilder();
+ sb.append("4004");
+ sb.append(hex("name"));
+ sb.append("7F813F");
+ for (int i = 0; i < 8192; i++) {
+ sb.append("61"); // 'a'
+ }
+ decode(sb.toString());
+ verifyNoMoreInteractions(mockListener);
+
+ // Verify header block is reported as truncated
+ assertTrue(decoder.endHeaderBlock());
+
+ // Verify next header is inserted at index 62
+ decode("4004" + hex("name") + "05" + hex("value") + "BE");
+ verify(mockListener, times(2)).addHeader(getBytes("name"), getBytes("value"), false);
+ verifyNoMoreInteractions(mockListener);
+ }
+
+ @Test(expected = IOException.class)
+ public void testLiteralWithoutIndexingWithEmptyName() throws Exception {
+ decode("000005" + hex("value"));
+ }
+
+ @Test(expected = IOException.class)
+ public void testLiteralWithoutIndexingWithLargeName() throws Exception {
+ // Ignore header name that exceeds max header size
+ StringBuilder sb = new StringBuilder();
+ sb.append("007F817F");
+ for (int i = 0; i < 16384; i++) {
+ sb.append("61"); // 'a'
+ }
+ sb.append("00");
+ decode(sb.toString());
+ verifyNoMoreInteractions(mockListener);
+
+ // Verify header block is reported as truncated
+ assertTrue(decoder.endHeaderBlock());
+
+ // Verify table is unmodified
+ decode("BE");
+ }
+
+ @Test(expected = IOException.class)
+ public void testLiteralWithoutIndexingWithLargeValue() throws Exception {
+ // Ignore header that exceeds max header size
+ StringBuilder sb = new StringBuilder();
+ sb.append("0004");
+ sb.append(hex("name"));
+ sb.append("7F813F");
+ for (int i = 0; i < 8192; i++) {
+ sb.append("61"); // 'a'
+ }
+ decode(sb.toString());
+ verifyNoMoreInteractions(mockListener);
+
+ // Verify header block is reported as truncated
+ assertTrue(decoder.endHeaderBlock());
+
+ // Verify table is unmodified
+ decode("BE");
+ }
+
+ @Test(expected = IOException.class)
+ public void testLiteralNeverIndexedWithEmptyName() throws Exception {
+ decode("100005" + hex("value"));
+ }
+
+ @Test(expected = IOException.class)
+ public void testLiteralNeverIndexedWithLargeName() throws Exception {
+ // Ignore header name that exceeds max header size
+ StringBuilder sb = new StringBuilder();
+ sb.append("107F817F");
+ for (int i = 0; i < 16384; i++) {
+ sb.append("61"); // 'a'
+ }
+ sb.append("00");
+ decode(sb.toString());
+ verifyNoMoreInteractions(mockListener);
+
+ // Verify header block is reported as truncated
+ assertTrue(decoder.endHeaderBlock());
+
+ // Verify table is unmodified
+ decode("BE");
+ }
+
+ @Test(expected = IOException.class)
+ public void testLiteralNeverIndexedWithLargeValue() throws Exception {
+ // Ignore header that exceeds max header size
+ StringBuilder sb = new StringBuilder();
+ sb.append("1004");
+ sb.append(hex("name"));
+ sb.append("7F813F");
+ for (int i = 0; i < 8192; i++) {
+ sb.append("61"); // 'a'
+ }
+ decode(sb.toString());
+ verifyNoMoreInteractions(mockListener);
+
+ // Verify header block is reported as truncated
+ assertTrue(decoder.endHeaderBlock());
+
+ // Verify table is unmodified
+ decode("BE");
+ }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/Hex.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/Hex.java
new file mode 100644
index 0000000000..f97561e163
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/Hex.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * 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 io.netty.handler.codec.http2.hpack;
+
+import java.io.IOException;
+
+/**
+ * Extracted from org/apache/commons/codec/binary/Hex.java Copyright Apache Software Foundation
+ */
+final class Hex {
+ private Hex() {
+ }
+
+ /**
+ * Used to build output as Hex
+ */
+ private static final char[] DIGITS_LOWER =
+ {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+ /**
+ * Used to build output as Hex
+ */
+ private static final char[] DIGITS_UPPER =
+ {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+ /**
+ * Converts an array of characters representing hexadecimal values into an array of bytes of
+ * those same values. The returned array will be half the length of the passed array, as it
+ * takes two characters to represent any given byte. An exception is thrown if the passed char
+ * array has an odd number of elements.
+ *
+ * @param data An array of characters containing hexadecimal digits
+ * @return A byte array containing binary data decoded from the supplied char array.
+ * @throws IOException Thrown if an odd number or illegal of characters is supplied
+ */
+ public static byte[] decodeHex(char[] data) throws IOException {
+
+ int len = data.length;
+
+ if ((len & 0x01) != 0) {
+ throw new IOException("Odd number of characters.");
+ }
+
+ byte[] out = new byte[len >> 1];
+
+ // two characters form the hex value.
+ for (int i = 0, j = 0; j < len; i++) {
+ int f = toDigit(data[j], j) << 4;
+ j++;
+ f = f | toDigit(data[j], j);
+ j++;
+ out[i] = (byte) (f & 0xFF);
+ }
+
+ return out;
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of
+ * each byte in order. The returned array will be double the length of the passed array, as it
+ * takes two characters to represent any given byte.
+ *
+ * @param data a byte[] to convert to Hex characters
+ * @return A char[] containing hexadecimal characters
+ */
+ public static char[] encodeHex(byte[] data) {
+ return encodeHex(data, true);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of
+ * each byte in order. The returned array will be double the length of the passed array, as it
+ * takes two characters to represent any given byte.
+ *
+ * @param data a byte[] to convert to Hex characters
+ * @param toLowerCase true converts to lowercase, false to uppercase
+ * @return A char[] containing hexadecimal characters
+ * @since 1.4
+ */
+ public static char[] encodeHex(byte[] data, boolean toLowerCase) {
+ return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+ }
+
+ /**
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of
+ * each byte in order. The returned array will be double the length of the passed array, as it
+ * takes two characters to represent any given byte.
+ *
+ * @param data a byte[] to convert to Hex characters
+ * @param toDigits the output alphabet
+ * @return A char[] containing hexadecimal characters
+ * @since 1.4
+ */
+ protected static char[] encodeHex(byte[] data, char[] toDigits) {
+ int l = data.length;
+ char[] out = new char[l << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < l; i++) {
+ out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
+ out[j++] = toDigits[0x0F & data[i]];
+ }
+ return out;
+ }
+
+ /**
+ * Converts an array of bytes into a String representing the hexadecimal values of each byte in
+ * order. The returned String will be double the length of the passed array, as it takes two
+ * characters to represent any given byte.
+ *
+ * @param data a byte[] to convert to Hex characters
+ * @return A String containing hexadecimal characters
+ * @since 1.4
+ */
+ public static String encodeHexString(byte[] data) {
+ return new String(encodeHex(data));
+ }
+
+ /**
+ * Converts a hexadecimal character to an integer.
+ *
+ * @param ch A character to convert to an integer digit
+ * @param index The index of the character in the source
+ * @return An integer
+ * @throws IOException Thrown if ch is an illegal hex character
+ */
+ protected static int toDigit(char ch, int index) throws IOException {
+ int digit = Character.digit(ch, 16);
+ if (digit == -1) {
+ throw new IOException("Illegal hexadecimal character " + ch + " at index " + index);
+ }
+ return digit;
+ }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HpackTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HpackTest.java
new file mode 100644
index 0000000000..794260eea7
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/hpack/HpackTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2015 The Netty Project
+ *
+ * The Netty Project 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.
+ */
+
+/*
+ * Copyright 2014 Twitter, Inc.
+ *
+ * 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.netty.handler.codec.http2.hpack;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class HpackTest {
+
+ private static final String TEST_DIR = "/io/netty/handler/codec/http2/hpack/testdata/";
+
+ private final String fileName;
+
+ public HpackTest(String fileName) {
+ this.fileName = fileName;
+ }
+
+ @Parameters(name = "{0}")
+ public static Collection