| /* |
| * Copyright (C) 2013 Square, 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 com.squareup.okhttp.internal.framed; |
| |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.List; |
| import okio.Buffer; |
| import okio.ByteString; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| import static com.squareup.okhttp.TestUtil.headerEntries; |
| import static okio.ByteString.decodeHex; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.fail; |
| |
| public class HpackTest { |
| |
| private final Buffer bytesIn = new Buffer(); |
| private Hpack.Reader hpackReader; |
| private Buffer bytesOut = new Buffer(); |
| private Hpack.Writer hpackWriter; |
| |
| @Before public void reset() { |
| hpackReader = newReader(bytesIn); |
| hpackWriter = new Hpack.Writer(bytesOut); |
| } |
| |
| /** |
| * Variable-length quantity special cases strings which are longer than 127 |
| * bytes. Values such as cookies can be 4KiB, and should be possible to send. |
| * |
| * <p> http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-5.2 |
| */ |
| @Test public void largeHeaderValue() throws IOException { |
| char[] value = new char[4096]; |
| Arrays.fill(value, '!'); |
| List<Header> headerBlock = headerEntries("cookie", new String(value)); |
| |
| hpackWriter.writeHeaders(headerBlock); |
| bytesIn.writeAll(bytesOut); |
| hpackReader.readHeaders(); |
| |
| assertEquals(0, hpackReader.headerCount); |
| |
| assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); |
| } |
| |
| /** |
| * HPACK has a max header table size, which can be smaller than the max header message. |
| * Ensure the larger header content is not lost. |
| */ |
| @Test public void tooLargeToHPackIsStillEmitted() throws IOException { |
| bytesIn.writeByte(0x00); // Literal indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-key"); |
| |
| bytesIn.writeByte(0x0d); // Literal value (len = 13) |
| bytesIn.writeUtf8("custom-header"); |
| |
| hpackReader.headerTableSizeSetting(1); |
| hpackReader.readHeaders(); |
| |
| assertEquals(0, hpackReader.headerCount); |
| |
| assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| /** Oldest entries are evicted to support newer ones. */ |
| @Test public void testEviction() throws IOException { |
| bytesIn.writeByte(0x40); // Literal indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-foo"); |
| |
| bytesIn.writeByte(0x0d); // Literal value (len = 13) |
| bytesIn.writeUtf8("custom-header"); |
| |
| bytesIn.writeByte(0x40); // Literal indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-bar"); |
| |
| bytesIn.writeByte(0x0d); // Literal value (len = 13) |
| bytesIn.writeUtf8("custom-header"); |
| |
| bytesIn.writeByte(0x40); // Literal indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-baz"); |
| |
| bytesIn.writeByte(0x0d); // Literal value (len = 13) |
| bytesIn.writeUtf8("custom-header"); |
| |
| // Set to only support 110 bytes (enough for 2 headers). |
| hpackReader.headerTableSizeSetting(110); |
| hpackReader.readHeaders(); |
| |
| assertEquals(2, hpackReader.headerCount); |
| |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, "custom-bar", "custom-header", 55); |
| |
| entry = hpackReader.dynamicTable[headerTableLength() - 2]; |
| checkEntry(entry, "custom-baz", "custom-header", 55); |
| |
| // Once a header field is decoded and added to the reconstructed header |
| // list, it cannot be removed from it. Hence, foo is here. |
| assertEquals( |
| headerEntries( |
| "custom-foo", "custom-header", |
| "custom-bar", "custom-header", |
| "custom-baz", "custom-header"), |
| hpackReader.getAndResetHeaderList()); |
| |
| // Simulate receiving a small settings frame, that implies eviction. |
| hpackReader.headerTableSizeSetting(55); |
| assertEquals(1, hpackReader.headerCount); |
| } |
| |
| /** Header table backing array is initially 8 long, let's ensure it grows. */ |
| @Test public void dynamicallyGrowsBeyond64Entries() throws IOException { |
| for (int i = 0; i < 256; i++) { |
| bytesIn.writeByte(0x40); // Literal indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-foo"); |
| |
| bytesIn.writeByte(0x0d); // Literal value (len = 13) |
| bytesIn.writeUtf8("custom-header"); |
| } |
| |
| hpackReader.headerTableSizeSetting(16384); // Lots of headers need more room! |
| hpackReader.readHeaders(); |
| |
| assertEquals(256, hpackReader.headerCount); |
| } |
| |
| @Test public void huffmanDecodingSupported() throws IOException { |
| bytesIn.writeByte(0x44); // == Literal indexed == |
| // Indexed name (idx = 4) -> :path |
| bytesIn.writeByte(0x8c); // Literal value Huffman encoded 12 bytes |
| // decodes to www.example.com which is length 15 |
| bytesIn.write(decodeHex("f1e3c2e5f23a6ba0ab90f4ff")); |
| |
| hpackReader.readHeaders(); |
| |
| assertEquals(1, hpackReader.headerCount); |
| assertEquals(52, hpackReader.dynamicTableByteCount); |
| |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, ":path", "www.example.com", 52); |
| } |
| |
| /** |
| * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.1 |
| */ |
| @Test public void readLiteralHeaderFieldWithIndexing() throws IOException { |
| bytesIn.writeByte(0x40); // Literal indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-key"); |
| |
| bytesIn.writeByte(0x0d); // Literal value (len = 13) |
| bytesIn.writeUtf8("custom-header"); |
| |
| hpackReader.readHeaders(); |
| |
| assertEquals(1, hpackReader.headerCount); |
| assertEquals(55, hpackReader.dynamicTableByteCount); |
| |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, "custom-key", "custom-header", 55); |
| |
| assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| /** |
| * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.2 |
| */ |
| @Test public void literalHeaderFieldWithoutIndexingIndexedName() throws IOException { |
| List<Header> headerBlock = headerEntries(":path", "/sample/path"); |
| |
| bytesIn.writeByte(0x04); // == Literal not indexed == |
| // Indexed name (idx = 4) -> :path |
| bytesIn.writeByte(0x0c); // Literal value (len = 12) |
| bytesIn.writeUtf8("/sample/path"); |
| |
| hpackWriter.writeHeaders(headerBlock); |
| assertEquals(bytesIn, bytesOut); |
| |
| hpackReader.readHeaders(); |
| |
| assertEquals(0, hpackReader.headerCount); |
| |
| assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); |
| } |
| |
| @Test public void literalHeaderFieldWithoutIndexingNewName() throws IOException { |
| List<Header> headerBlock = headerEntries("custom-key", "custom-header"); |
| |
| bytesIn.writeByte(0x00); // Not indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-key"); |
| |
| bytesIn.writeByte(0x0d); // Literal value (len = 13) |
| bytesIn.writeUtf8("custom-header"); |
| |
| hpackWriter.writeHeaders(headerBlock); |
| assertEquals(bytesIn, bytesOut); |
| |
| hpackReader.readHeaders(); |
| |
| assertEquals(0, hpackReader.headerCount); |
| |
| assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); |
| } |
| |
| @Test public void literalHeaderFieldNeverIndexedIndexedName() throws IOException { |
| bytesIn.writeByte(0x14); // == Literal never indexed == |
| // Indexed name (idx = 4) -> :path |
| bytesIn.writeByte(0x0c); // Literal value (len = 12) |
| bytesIn.writeUtf8("/sample/path"); |
| |
| hpackReader.readHeaders(); |
| |
| assertEquals(0, hpackReader.headerCount); |
| |
| assertEquals(headerEntries(":path", "/sample/path"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| @Test public void literalHeaderFieldNeverIndexedNewName() throws IOException { |
| bytesIn.writeByte(0x10); // Never indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-key"); |
| |
| bytesIn.writeByte(0x0d); // Literal value (len = 13) |
| bytesIn.writeUtf8("custom-header"); |
| |
| hpackReader.readHeaders(); |
| |
| assertEquals(0, hpackReader.headerCount); |
| |
| assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| @Test public void staticHeaderIsNotCopiedIntoTheIndexedTable() throws IOException { |
| bytesIn.writeByte(0x82); // == Indexed - Add == |
| // idx = 2 -> :method: GET |
| |
| hpackReader.readHeaders(); |
| |
| assertEquals(0, hpackReader.headerCount); |
| assertEquals(0, hpackReader.dynamicTableByteCount); |
| |
| assertEquals(null, hpackReader.dynamicTable[headerTableLength() - 1]); |
| |
| assertEquals(headerEntries(":method", "GET"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| // Example taken from twitter/hpack DecoderTest.testUnusedIndex |
| @Test public void readIndexedHeaderFieldIndex0() throws IOException { |
| bytesIn.writeByte(0x80); // == Indexed - Add idx = 0 |
| |
| try { |
| hpackReader.readHeaders(); |
| fail(); |
| } catch (IOException e) { |
| assertEquals("index == 0", e.getMessage()); |
| } |
| } |
| |
| // Example taken from twitter/hpack DecoderTest.testIllegalIndex |
| @Test public void readIndexedHeaderFieldTooLargeIndex() throws IOException { |
| bytesIn.writeShort(0xff00); // == Indexed - Add idx = 127 |
| |
| try { |
| hpackReader.readHeaders(); |
| fail(); |
| } catch (IOException e) { |
| assertEquals("Header index too large 127", e.getMessage()); |
| } |
| } |
| |
| // Example taken from twitter/hpack DecoderTest.testInsidiousIndex |
| @Test public void readIndexedHeaderFieldInsidiousIndex() throws IOException { |
| bytesIn.writeByte(0xff); // == Indexed - Add == |
| bytesIn.write(decodeHex("8080808008")); // idx = -2147483521 |
| |
| try { |
| hpackReader.readHeaders(); |
| fail(); |
| } catch (IOException e) { |
| assertEquals("Header index too large -2147483521", e.getMessage()); |
| } |
| } |
| |
| // Example taken from twitter/hpack DecoderTest.testHeaderTableSizeUpdate |
| @Test public void minMaxHeaderTableSize() throws IOException { |
| bytesIn.writeByte(0x20); |
| hpackReader.readHeaders(); |
| |
| assertEquals(0, hpackReader.maxDynamicTableByteCount()); |
| |
| bytesIn.writeByte(0x3f); // encode size 4096 |
| bytesIn.writeByte(0xe1); |
| bytesIn.writeByte(0x1f); |
| hpackReader.readHeaders(); |
| |
| assertEquals(4096, hpackReader.maxDynamicTableByteCount()); |
| } |
| |
| // Example taken from twitter/hpack DecoderTest.testIllegalHeaderTableSizeUpdate |
| @Test public void cannotSetTableSizeLargerThanSettingsValue() throws IOException { |
| bytesIn.writeByte(0x3f); // encode size 4097 |
| bytesIn.writeByte(0xe2); |
| bytesIn.writeByte(0x1f); |
| |
| try { |
| hpackReader.readHeaders(); |
| fail(); |
| } catch (IOException e) { |
| assertEquals("Invalid dynamic table size update 4097", e.getMessage()); |
| } |
| } |
| |
| // Example taken from twitter/hpack DecoderTest.testInsidiousMaxHeaderSize |
| @Test public void readHeaderTableStateChangeInsidiousMaxHeaderByteCount() throws IOException { |
| bytesIn.writeByte(0x3f); |
| bytesIn.write(decodeHex("e1ffffff07")); // count = -2147483648 |
| |
| try { |
| hpackReader.readHeaders(); |
| fail(); |
| } catch (IOException e) { |
| assertEquals("Invalid dynamic table size update -2147483648", e.getMessage()); |
| } |
| } |
| |
| /** |
| * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.4 |
| */ |
| @Test public void readIndexedHeaderFieldFromStaticTableWithoutBuffering() throws IOException { |
| bytesIn.writeByte(0x82); // == Indexed - Add == |
| // idx = 2 -> :method: GET |
| |
| hpackReader.headerTableSizeSetting(0); // SETTINGS_HEADER_TABLE_SIZE == 0 |
| hpackReader.readHeaders(); |
| |
| // Not buffered in header table. |
| assertEquals(0, hpackReader.headerCount); |
| |
| assertEquals(headerEntries(":method", "GET"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| /** |
| * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2 |
| */ |
| @Test public void readRequestExamplesWithoutHuffman() throws IOException { |
| firstRequestWithoutHuffman(); |
| hpackReader.readHeaders(); |
| checkReadFirstRequestWithoutHuffman(); |
| |
| secondRequestWithoutHuffman(); |
| hpackReader.readHeaders(); |
| checkReadSecondRequestWithoutHuffman(); |
| |
| thirdRequestWithoutHuffman(); |
| hpackReader.readHeaders(); |
| checkReadThirdRequestWithoutHuffman(); |
| } |
| |
| private void firstRequestWithoutHuffman() { |
| bytesIn.writeByte(0x82); // == Indexed - Add == |
| // idx = 2 -> :method: GET |
| bytesIn.writeByte(0x86); // == Indexed - Add == |
| // idx = 7 -> :scheme: http |
| bytesIn.writeByte(0x84); // == Indexed - Add == |
| // idx = 6 -> :path: / |
| bytesIn.writeByte(0x41); // == Literal indexed == |
| // Indexed name (idx = 4) -> :authority |
| bytesIn.writeByte(0x0f); // Literal value (len = 15) |
| bytesIn.writeUtf8("www.example.com"); |
| } |
| |
| private void checkReadFirstRequestWithoutHuffman() { |
| assertEquals(1, hpackReader.headerCount); |
| |
| // [ 1] (s = 57) :authority: www.example.com |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, ":authority", "www.example.com", 57); |
| |
| // Table size: 57 |
| assertEquals(57, hpackReader.dynamicTableByteCount); |
| |
| // Decoded header list: |
| assertEquals(headerEntries( |
| ":method", "GET", |
| ":scheme", "http", |
| ":path", "/", |
| ":authority", "www.example.com"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| private void secondRequestWithoutHuffman() { |
| bytesIn.writeByte(0x82); // == Indexed - Add == |
| // idx = 2 -> :method: GET |
| bytesIn.writeByte(0x86); // == Indexed - Add == |
| // idx = 7 -> :scheme: http |
| bytesIn.writeByte(0x84); // == Indexed - Add == |
| // idx = 6 -> :path: / |
| bytesIn.writeByte(0xbe); // == Indexed - Add == |
| // Indexed name (idx = 62) -> :authority: www.example.com |
| bytesIn.writeByte(0x58); // == Literal indexed == |
| // Indexed name (idx = 24) -> cache-control |
| bytesIn.writeByte(0x08); // Literal value (len = 8) |
| bytesIn.writeUtf8("no-cache"); |
| } |
| |
| private void checkReadSecondRequestWithoutHuffman() { |
| assertEquals(2, hpackReader.headerCount); |
| |
| // [ 1] (s = 53) cache-control: no-cache |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 2]; |
| checkEntry(entry, "cache-control", "no-cache", 53); |
| |
| // [ 2] (s = 57) :authority: www.example.com |
| entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, ":authority", "www.example.com", 57); |
| |
| // Table size: 110 |
| assertEquals(110, hpackReader.dynamicTableByteCount); |
| |
| // Decoded header list: |
| assertEquals(headerEntries( |
| ":method", "GET", |
| ":scheme", "http", |
| ":path", "/", |
| ":authority", "www.example.com", |
| "cache-control", "no-cache"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| private void thirdRequestWithoutHuffman() { |
| bytesIn.writeByte(0x82); // == Indexed - Add == |
| // idx = 2 -> :method: GET |
| bytesIn.writeByte(0x87); // == Indexed - Add == |
| // idx = 7 -> :scheme: http |
| bytesIn.writeByte(0x85); // == Indexed - Add == |
| // idx = 5 -> :path: /index.html |
| bytesIn.writeByte(0xbf); // == Indexed - Add == |
| // Indexed name (idx = 63) -> :authority: www.example.com |
| bytesIn.writeByte(0x40); // Literal indexed |
| bytesIn.writeByte(0x0a); // Literal name (len = 10) |
| bytesIn.writeUtf8("custom-key"); |
| bytesIn.writeByte(0x0c); // Literal value (len = 12) |
| bytesIn.writeUtf8("custom-value"); |
| } |
| |
| private void checkReadThirdRequestWithoutHuffman() { |
| assertEquals(3, hpackReader.headerCount); |
| |
| // [ 1] (s = 54) custom-key: custom-value |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 3]; |
| checkEntry(entry, "custom-key", "custom-value", 54); |
| |
| // [ 2] (s = 53) cache-control: no-cache |
| entry = hpackReader.dynamicTable[headerTableLength() - 2]; |
| checkEntry(entry, "cache-control", "no-cache", 53); |
| |
| // [ 3] (s = 57) :authority: www.example.com |
| entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, ":authority", "www.example.com", 57); |
| |
| // Table size: 164 |
| assertEquals(164, hpackReader.dynamicTableByteCount); |
| |
| // Decoded header list: |
| assertEquals(headerEntries( |
| ":method", "GET", |
| ":scheme", "https", |
| ":path", "/index.html", |
| ":authority", "www.example.com", |
| "custom-key", "custom-value"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| /** |
| * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.4 |
| */ |
| @Test public void readRequestExamplesWithHuffman() throws IOException { |
| firstRequestWithHuffman(); |
| hpackReader.readHeaders(); |
| checkReadFirstRequestWithHuffman(); |
| |
| secondRequestWithHuffman(); |
| hpackReader.readHeaders(); |
| checkReadSecondRequestWithHuffman(); |
| |
| thirdRequestWithHuffman(); |
| hpackReader.readHeaders(); |
| checkReadThirdRequestWithHuffman(); |
| } |
| |
| private void firstRequestWithHuffman() { |
| bytesIn.writeByte(0x82); // == Indexed - Add == |
| // idx = 2 -> :method: GET |
| bytesIn.writeByte(0x86); // == Indexed - Add == |
| // idx = 6 -> :scheme: http |
| bytesIn.writeByte(0x84); // == Indexed - Add == |
| // idx = 4 -> :path: / |
| bytesIn.writeByte(0x41); // == Literal indexed == |
| // Indexed name (idx = 1) -> :authority |
| bytesIn.writeByte(0x8c); // Literal value Huffman encoded 12 bytes |
| // decodes to www.example.com which is length 15 |
| bytesIn.write(decodeHex("f1e3c2e5f23a6ba0ab90f4ff")); |
| } |
| |
| private void checkReadFirstRequestWithHuffman() { |
| assertEquals(1, hpackReader.headerCount); |
| |
| // [ 1] (s = 57) :authority: www.example.com |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, ":authority", "www.example.com", 57); |
| |
| // Table size: 57 |
| assertEquals(57, hpackReader.dynamicTableByteCount); |
| |
| // Decoded header list: |
| assertEquals(headerEntries( |
| ":method", "GET", |
| ":scheme", "http", |
| ":path", "/", |
| ":authority", "www.example.com"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| private void secondRequestWithHuffman() { |
| bytesIn.writeByte(0x82); // == Indexed - Add == |
| // idx = 2 -> :method: GET |
| bytesIn.writeByte(0x86); // == Indexed - Add == |
| // idx = 6 -> :scheme: http |
| bytesIn.writeByte(0x84); // == Indexed - Add == |
| // idx = 4 -> :path: / |
| bytesIn.writeByte(0xbe); // == Indexed - Add == |
| // idx = 62 -> :authority: www.example.com |
| bytesIn.writeByte(0x58); // == Literal indexed == |
| // Indexed name (idx = 24) -> cache-control |
| bytesIn.writeByte(0x86); // Literal value Huffman encoded 6 bytes |
| // decodes to no-cache which is length 8 |
| bytesIn.write(decodeHex("a8eb10649cbf")); |
| } |
| |
| private void checkReadSecondRequestWithHuffman() { |
| assertEquals(2, hpackReader.headerCount); |
| |
| // [ 1] (s = 53) cache-control: no-cache |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 2]; |
| checkEntry(entry, "cache-control", "no-cache", 53); |
| |
| // [ 2] (s = 57) :authority: www.example.com |
| entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, ":authority", "www.example.com", 57); |
| |
| // Table size: 110 |
| assertEquals(110, hpackReader.dynamicTableByteCount); |
| |
| // Decoded header list: |
| assertEquals(headerEntries( |
| ":method", "GET", |
| ":scheme", "http", |
| ":path", "/", |
| ":authority", "www.example.com", |
| "cache-control", "no-cache"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| private void thirdRequestWithHuffman() { |
| bytesIn.writeByte(0x82); // == Indexed - Add == |
| // idx = 2 -> :method: GET |
| bytesIn.writeByte(0x87); // == Indexed - Add == |
| // idx = 7 -> :scheme: https |
| bytesIn.writeByte(0x85); // == Indexed - Add == |
| // idx = 5 -> :path: /index.html |
| bytesIn.writeByte(0xbf); // == Indexed - Add == |
| // idx = 63 -> :authority: www.example.com |
| bytesIn.writeByte(0x40); // Literal indexed |
| bytesIn.writeByte(0x88); // Literal name Huffman encoded 8 bytes |
| // decodes to custom-key which is length 10 |
| bytesIn.write(decodeHex("25a849e95ba97d7f")); |
| bytesIn.writeByte(0x89); // Literal value Huffman encoded 9 bytes |
| // decodes to custom-value which is length 12 |
| bytesIn.write(decodeHex("25a849e95bb8e8b4bf")); |
| } |
| |
| private void checkReadThirdRequestWithHuffman() { |
| assertEquals(3, hpackReader.headerCount); |
| |
| // [ 1] (s = 54) custom-key: custom-value |
| Header entry = hpackReader.dynamicTable[headerTableLength() - 3]; |
| checkEntry(entry, "custom-key", "custom-value", 54); |
| |
| // [ 2] (s = 53) cache-control: no-cache |
| entry = hpackReader.dynamicTable[headerTableLength() - 2]; |
| checkEntry(entry, "cache-control", "no-cache", 53); |
| |
| // [ 3] (s = 57) :authority: www.example.com |
| entry = hpackReader.dynamicTable[headerTableLength() - 1]; |
| checkEntry(entry, ":authority", "www.example.com", 57); |
| |
| // Table size: 164 |
| assertEquals(164, hpackReader.dynamicTableByteCount); |
| |
| // Decoded header list: |
| assertEquals(headerEntries( |
| ":method", "GET", |
| ":scheme", "https", |
| ":path", "/index.html", |
| ":authority", "www.example.com", |
| "custom-key", "custom-value"), hpackReader.getAndResetHeaderList()); |
| } |
| |
| @Test public void readSingleByteInt() throws IOException { |
| assertEquals(10, newReader(byteStream()).readInt(10, 31)); |
| assertEquals(10, newReader(byteStream()).readInt(0xe0 | 10, 31)); |
| } |
| |
| @Test public void readMultibyteInt() throws IOException { |
| assertEquals(1337, newReader(byteStream(154, 10)).readInt(31, 31)); |
| } |
| |
| @Test public void writeSingleByteInt() throws IOException { |
| hpackWriter.writeInt(10, 31, 0); |
| assertBytes(10); |
| hpackWriter.writeInt(10, 31, 0xe0); |
| assertBytes(0xe0 | 10); |
| } |
| |
| @Test public void writeMultibyteInt() throws IOException { |
| hpackWriter.writeInt(1337, 31, 0); |
| assertBytes(31, 154, 10); |
| hpackWriter.writeInt(1337, 31, 0xe0); |
| assertBytes(0xe0 | 31, 154, 10); |
| } |
| |
| @Test public void max31BitValue() throws IOException { |
| hpackWriter.writeInt(0x7fffffff, 31, 0); |
| assertBytes(31, 224, 255, 255, 255, 7); |
| assertEquals(0x7fffffff, |
| newReader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31)); |
| } |
| |
| @Test public void prefixMask() throws IOException { |
| hpackWriter.writeInt(31, 31, 0); |
| assertBytes(31, 0); |
| assertEquals(31, newReader(byteStream(0)).readInt(31, 31)); |
| } |
| |
| @Test public void prefixMaskMinusOne() throws IOException { |
| hpackWriter.writeInt(30, 31, 0); |
| assertBytes(30); |
| assertEquals(31, newReader(byteStream(0)).readInt(31, 31)); |
| } |
| |
| @Test public void zero() throws IOException { |
| hpackWriter.writeInt(0, 31, 0); |
| assertBytes(0); |
| assertEquals(0, newReader(byteStream()).readInt(0, 31)); |
| } |
| |
| @Test public void lowercaseHeaderNameBeforeEmit() throws IOException { |
| hpackWriter.writeHeaders(Arrays.asList(new Header("FoO", "BaR"))); |
| assertBytes(0, 3, 'f', 'o', 'o', 3, 'B', 'a', 'R'); |
| } |
| |
| @Test public void mixedCaseHeaderNameIsMalformed() throws IOException { |
| try { |
| newReader(byteStream(0, 3, 'F', 'o', 'o', 3, 'B', 'a', 'R')).readHeaders(); |
| fail(); |
| } catch (IOException e) { |
| assertEquals("PROTOCOL_ERROR response malformed: mixed case name: Foo", e.getMessage()); |
| } |
| } |
| |
| @Test public void emptyHeaderName() throws IOException { |
| hpackWriter.writeByteString(ByteString.encodeUtf8("")); |
| assertBytes(0); |
| assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString()); |
| } |
| |
| private Hpack.Reader newReader(Buffer source) { |
| return new Hpack.Reader(4096, source); |
| } |
| |
| private Buffer byteStream(int... bytes) { |
| return new Buffer().write(intArrayToByteArray(bytes)); |
| } |
| |
| private void checkEntry(Header entry, String name, String value, int size) { |
| assertEquals(name, entry.name.utf8()); |
| assertEquals(value, entry.value.utf8()); |
| assertEquals(size, entry.hpackSize); |
| } |
| |
| private void assertBytes(int... bytes) throws IOException { |
| ByteString expected = intArrayToByteArray(bytes); |
| ByteString actual = bytesOut.readByteString(); |
| assertEquals(expected, actual); |
| } |
| |
| private ByteString intArrayToByteArray(int[] bytes) { |
| byte[] data = new byte[bytes.length]; |
| for (int i = 0; i < bytes.length; i++) { |
| data[i] = (byte) bytes[i]; |
| } |
| return ByteString.of(data); |
| } |
| |
| private int headerTableLength() { |
| return hpackReader.dynamicTable.length; |
| } |
| } |