diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java index 80065e486..63a504624 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java @@ -1,6 +1,7 @@ package com.squareup.okhttp.internal.spdy; import com.squareup.okhttp.internal.ByteString; +import com.squareup.okhttp.internal.Util; import java.io.DataInputStream; import java.io.IOException; import java.io.OutputStream; @@ -115,6 +116,8 @@ final class HpackDraft05 { // TODO: huffman encoding! // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.2 static class Reader { + private final Huffman.Codec huffmanCodec; + private final DataInputStream in; private final List emittedHeaders = new ArrayList(); private long bytesLeft = 0; @@ -140,7 +143,8 @@ final class HpackDraft05 { int headerTableByteCount = 0; int maxHeaderTableByteCount = 4096; // TODO: needs to come from SETTINGS_HEADER_TABLE_SIZE. - Reader(DataInputStream in) { + Reader(boolean client, DataInputStream in) { + this.huffmanCodec = client ? Huffman.Codec.RESPONSE : Huffman.Codec.REQUEST; this.in = in; } @@ -371,7 +375,10 @@ final class HpackDraft05 { int length = readInt(firstByte, PREFIX_8_BITS); if ((length & 0x80) == 0x80) { // 1NNNNNNN length &= ~0x80; - // TODO: actually decode huffman! + byte[] buff = new byte[length]; + Util.readFully(in, buff); + bytesLeft -= length; + return ByteString.of(huffmanCodec.decode(buff)); } bytesLeft -= length; return ByteString.read(in, length); diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java index 1cfb93fd5..b12433868 100644 --- a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java @@ -83,7 +83,7 @@ public final class Http20Draft09 implements Variant { Reader(InputStream in, boolean client) { this.in = new DataInputStream(in); this.client = client; - this.hpackReader = new HpackDraft05.Reader(this.in); + this.hpackReader = new HpackDraft05.Reader(client, this.in); } @Override public void readConnectionHeader() throws IOException { diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java new file mode 100644 index 000000000..a7768d0ae --- /dev/null +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java @@ -0,0 +1,279 @@ +/* + * Copyright 2013 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 com.squareup.okhttp.internal.spdy; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class was originally composed from the following classes in + * Twitter Hpack. + * + */ +class Huffman { + enum Codec { + REQUEST(REQUEST_CODES, REQUEST_CODE_LENGTHS), + RESPONSE(RESPONSE_CODES, RESPONSE_CODE_LENGTHS); + + private final Node root = new Node(); + private final int[] codes; + private final byte[] lengths; + + /** + * @param codes Index designates the symbol this code represents. + * @param lengths Index designates the symbol this code represents. + */ + Codec(int[] codes, byte[] lengths) { + buildTree(codes, lengths); + this.codes = codes; + this.lengths = lengths; + } + + void encode(byte[] data, OutputStream out) throws IOException { + long current = 0; + int n = 0; + + for (int i = 0; i < data.length; i++) { + int b = data[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); + out.write((int) current); + } + } + + int encodedLength(byte[] bytes) { + long len = 0; + + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i] & 0xFF; + len += lengths[b]; + } + + return (int) ((len + 7) >> 3); + } + + byte[] decode(byte[] buf) throws IOException { + // FIXME + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Node node = root; + int current = 0; + int nbits = 0; + for (int i = 0; i < buf.length; i++) { + int b = buf[i] & 0xFF; + current = (current << 8) | b; + nbits += 8; + while (nbits >= 8) { + int c = (current >>> (nbits - 8)) & 0xFF; + node = node.children[c]; + if (node.children == null) { + // terminal node + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } else { + // non-terminal node + nbits -= 8; + } + } + } + + while (nbits > 0) { + int c = (current << (8 - nbits)) & 0xFF; + node = node.children[c]; + if (node.children != null || node.terminalBits > nbits) { + break; + } + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } + + return baos.toByteArray(); + } + + private void buildTree(int[] codes, byte[] lengths) { + for (int i = 0; i < lengths.length; i++) { + addCode(i, codes[i], lengths[i]); + } + } + + private void addCode(int sym, int code, byte len) { + Node terminal = new Node(sym, len); + + Node current = root; + while (len > 8) { + len -= 8; + int i = ((code >>> len) & 0xFF); + if (current.children == null) { + throw new IllegalStateException("invalid dictionary: prefix not unique"); + } + if (current.children[i] == null) { + current.children[i] = new Node(); + } + current = current.children[i]; + } + + int shift = 8 - len; + int start = (code << shift) & 0xFF; + int end = 1 << shift; + for (int i = start; i < start + end; i++) { + current.children[i] = terminal; + } + } + } + + private static final class Node { + + // Null if terminal. + private final Node[] children; + + // Terminal nodes have a symbol. + private final int symbol; + + // Number of bits represented in the terminal node. + private final int terminalBits; + + /** Construct an internal node. */ + Node() { + this.children = new Node[256]; + this.symbol = 0; // Not read. + this.terminalBits = 0; // Not read. + } + + /** + * Construct a terminal node. + * + * @param symbol symbol the node represents + * @param bits length of Huffman code in bits + */ + Node(int symbol, int bits) { + this.children = null; + this.symbol = symbol; + int b = bits & 0x07; + this.terminalBits = b == 0 ? 8 : b; + } + } + + // Appendix C: Huffman Codes For Requests + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-C + private static final int[] REQUEST_CODES = { + 0x7ffffba, 0x7ffffbb, 0x7ffffbc, 0x7ffffbd, 0x7ffffbe, 0x7ffffbf, 0x7ffffc0, 0x7ffffc1, + 0x7ffffc2, 0x7ffffc3, 0x7ffffc4, 0x7ffffc5, 0x7ffffc6, 0x7ffffc7, 0x7ffffc8, 0x7ffffc9, + 0x7ffffca, 0x7ffffcb, 0x7ffffcc, 0x7ffffcd, 0x7ffffce, 0x7ffffcf, 0x7ffffd0, 0x7ffffd1, + 0x7ffffd2, 0x7ffffd3, 0x7ffffd4, 0x7ffffd5, 0x7ffffd6, 0x7ffffd7, 0x7ffffd8, 0x7ffffd9, 0xe8, + 0xffc, 0x3ffa, 0x7ffc, 0x7ffd, 0x24, 0x6e, 0x7ffe, 0x7fa, 0x7fb, 0x3fa, 0x7fc, 0xe9, 0x25, + 0x4, 0x0, 0x5, 0x6, 0x7, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x1ec, 0xea, 0x3fffe, 0x2d, + 0x1fffc, 0x1ed, 0x3ffb, 0x6f, 0xeb, 0xec, 0xed, 0xee, 0x70, 0x1ee, 0x1ef, 0x1f0, 0x1f1, 0x3fb, + 0x1f2, 0xef, 0x1f3, 0x1f4, 0x1f5, 0x1f6, 0x1f7, 0xf0, 0xf1, 0x1f8, 0x1f9, 0x1fa, 0x1fb, 0x1fc, + 0x3fc, 0x3ffc, 0x7ffffda, 0x1ffc, 0x3ffd, 0x2e, 0x7fffe, 0x8, 0x2f, 0x9, 0x30, 0x1, 0x31, + 0x32, 0x33, 0xa, 0x71, 0x72, 0xb, 0x34, 0xc, 0xd, 0xe, 0xf2, 0xf, 0x10, 0x11, 0x35, 0x73, + 0x36, 0xf3, 0xf4, 0xf5, 0x1fffd, 0x7fd, 0x1fffe, 0xffd, 0x7ffffdb, 0x7ffffdc, 0x7ffffdd, + 0x7ffffde, 0x7ffffdf, 0x7ffffe0, 0x7ffffe1, 0x7ffffe2, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5, + 0x7ffffe6, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0x7ffffec, 0x7ffffed, + 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x7fffff1, 0x7fffff2, 0x7fffff3, 0x7fffff4, 0x7fffff5, + 0x7fffff6, 0x7fffff7, 0x7fffff8, 0x7fffff9, 0x7fffffa, 0x7fffffb, 0x7fffffc, 0x7fffffd, + 0x7fffffe, 0x7ffffff, 0x3ffff80, 0x3ffff81, 0x3ffff82, 0x3ffff83, 0x3ffff84, 0x3ffff85, + 0x3ffff86, 0x3ffff87, 0x3ffff88, 0x3ffff89, 0x3ffff8a, 0x3ffff8b, 0x3ffff8c, 0x3ffff8d, + 0x3ffff8e, 0x3ffff8f, 0x3ffff90, 0x3ffff91, 0x3ffff92, 0x3ffff93, 0x3ffff94, 0x3ffff95, + 0x3ffff96, 0x3ffff97, 0x3ffff98, 0x3ffff99, 0x3ffff9a, 0x3ffff9b, 0x3ffff9c, 0x3ffff9d, + 0x3ffff9e, 0x3ffff9f, 0x3ffffa0, 0x3ffffa1, 0x3ffffa2, 0x3ffffa3, 0x3ffffa4, 0x3ffffa5, + 0x3ffffa6, 0x3ffffa7, 0x3ffffa8, 0x3ffffa9, 0x3ffffaa, 0x3ffffab, 0x3ffffac, 0x3ffffad, + 0x3ffffae, 0x3ffffaf, 0x3ffffb0, 0x3ffffb1, 0x3ffffb2, 0x3ffffb3, 0x3ffffb4, 0x3ffffb5, + 0x3ffffb6, 0x3ffffb7, 0x3ffffb8, 0x3ffffb9, 0x3ffffba, 0x3ffffbb, 0x3ffffbc, 0x3ffffbd, + 0x3ffffbe, 0x3ffffbf, 0x3ffffc0, 0x3ffffc1, 0x3ffffc2, 0x3ffffc3, 0x3ffffc4, 0x3ffffc5, + 0x3ffffc6, 0x3ffffc7, 0x3ffffc8, 0x3ffffc9, 0x3ffffca, 0x3ffffcb, 0x3ffffcc, 0x3ffffcd, + 0x3ffffce, 0x3ffffcf, 0x3ffffd0, 0x3ffffd1, 0x3ffffd2, 0x3ffffd3, 0x3ffffd4, 0x3ffffd5, + 0x3ffffd6, 0x3ffffd7, 0x3ffffd8, 0x3ffffd9, 0x3ffffda, 0x3ffffdb + }; + + private static final byte[] REQUEST_CODE_LENGTHS = { + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 8, 12, 14, 15, 15, 6, 7, 15, 11, 11, 10, 11, 8, 6, 5, 4, + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 9, 8, 18, 6, 17, 9, 14, 7, 8, 8, 8, 8, 7, 9, 9, 9, 9, 10, 9, 8, + 9, 9, 9, 9, 9, 8, 8, 9, 9, 9, 9, 9, 10, 14, 27, 13, 14, 6, 19, 5, 6, 5, 6, 4, 6, 6, 6, 5, 7, + 7, 5, 6, 5, 5, 5, 8, 5, 5, 5, 6, 7, 6, 8, 8, 8, 17, 11, 17, 12, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, + 27, 27, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26 + }; + + // Appendix D: Huffman Codes For Responses + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-D + private static final int[] RESPONSE_CODES = { + 0x1ffffbc, 0x1ffffbd, 0x1ffffbe, 0x1ffffbf, 0x1ffffc0, 0x1ffffc1, 0x1ffffc2, 0x1ffffc3, + 0x1ffffc4, 0x1ffffc5, 0x1ffffc6, 0x1ffffc7, 0x1ffffc8, 0x1ffffc9, 0x1ffffca, 0x1ffffcb, + 0x1ffffcc, 0x1ffffcd, 0x1ffffce, 0x1ffffcf, 0x1ffffd0, 0x1ffffd1, 0x1ffffd2, 0x1ffffd3, + 0x1ffffd4, 0x1ffffd5, 0x1ffffd6, 0x1ffffd7, 0x1ffffd8, 0x1ffffd9, 0x1ffffda, 0x1ffffdb, 0x0, + 0xffa, 0x6a, 0x1ffa, 0x3ffc, 0x1ec, 0x3f8, 0x1ffb, 0x1ed, 0x1ee, 0xffb, 0x7fa, 0x22, 0x23, + 0x24, 0x6b, 0x1, 0x2, 0x3, 0x8, 0x9, 0xa, 0x25, 0x26, 0xb, 0xc, 0xd, 0x1ef, 0xfffa, 0x6c, + 0x1ffc, 0xffc, 0xfffb, 0x6d, 0xea, 0xeb, 0xec, 0xed, 0xee, 0x27, 0x1f0, 0xef, 0xf0, 0x3f9, + 0x1f1, 0x28, 0xf1, 0xf2, 0x1f2, 0x3fa, 0x1f3, 0x29, 0xe, 0x1f4, 0x1f5, 0xf3, 0x3fb, 0x1f6, + 0x3fc, 0x7fb, 0x1ffd, 0x7fc, 0x7ffc, 0x1f7, 0x1fffe, 0xf, 0x6e, 0x2a, 0x2b, 0x10, 0x6f, 0x70, + 0x71, 0x2c, 0x1f8, 0x1f9, 0x72, 0x2d, 0x2e, 0x2f, 0x30, 0x1fa, 0x31, 0x32, 0x33, 0x34, 0x73, + 0xf4, 0x74, 0xf5, 0x1fb, 0xfffc, 0x3ffd, 0xfffd, 0xfffe, 0x1ffffdc, 0x1ffffdd, 0x1ffffde, + 0x1ffffdf, 0x1ffffe0, 0x1ffffe1, 0x1ffffe2, 0x1ffffe3, 0x1ffffe4, 0x1ffffe5, 0x1ffffe6, + 0x1ffffe7, 0x1ffffe8, 0x1ffffe9, 0x1ffffea, 0x1ffffeb, 0x1ffffec, 0x1ffffed, 0x1ffffee, + 0x1ffffef, 0x1fffff0, 0x1fffff1, 0x1fffff2, 0x1fffff3, 0x1fffff4, 0x1fffff5, 0x1fffff6, + 0x1fffff7, 0x1fffff8, 0x1fffff9, 0x1fffffa, 0x1fffffb, 0x1fffffc, 0x1fffffd, 0x1fffffe, + 0x1ffffff, 0xffff80, 0xffff81, 0xffff82, 0xffff83, 0xffff84, 0xffff85, 0xffff86, 0xffff87, + 0xffff88, 0xffff89, 0xffff8a, 0xffff8b, 0xffff8c, 0xffff8d, 0xffff8e, 0xffff8f, 0xffff90, + 0xffff91, 0xffff92, 0xffff93, 0xffff94, 0xffff95, 0xffff96, 0xffff97, 0xffff98, 0xffff99, + 0xffff9a, 0xffff9b, 0xffff9c, 0xffff9d, 0xffff9e, 0xffff9f, 0xffffa0, 0xffffa1, 0xffffa2, + 0xffffa3, 0xffffa4, 0xffffa5, 0xffffa6, 0xffffa7, 0xffffa8, 0xffffa9, 0xffffaa, 0xffffab, + 0xffffac, 0xffffad, 0xffffae, 0xffffaf, 0xffffb0, 0xffffb1, 0xffffb2, 0xffffb3, 0xffffb4, + 0xffffb5, 0xffffb6, 0xffffb7, 0xffffb8, 0xffffb9, 0xffffba, 0xffffbb, 0xffffbc, 0xffffbd, + 0xffffbe, 0xffffbf, 0xffffc0, 0xffffc1, 0xffffc2, 0xffffc3, 0xffffc4, 0xffffc5, 0xffffc6, + 0xffffc7, 0xffffc8, 0xffffc9, 0xffffca, 0xffffcb, 0xffffcc, 0xffffcd, 0xffffce, 0xffffcf, + 0xffffd0, 0xffffd1, 0xffffd2, 0xffffd3, 0xffffd4, 0xffffd5, 0xffffd6, 0xffffd7, 0xffffd8, + 0xffffd9, 0xffffda, 0xffffdb, 0xffffdc + }; + + private static final byte[] RESPONSE_CODE_LENGTHS = { + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 4, 12, 7, 13, 14, 9, 10, 13, 9, 9, 12, 11, 6, 6, 6, 7, 4, + 4, 4, 5, 5, 5, 6, 6, 5, 5, 5, 9, 16, 7, 13, 12, 16, 7, 8, 8, 8, 8, 8, 6, 9, 8, 8, 10, 9, 6, 8, + 8, 9, 10, 9, 6, 5, 9, 9, 8, 10, 9, 10, 11, 13, 11, 15, 9, 17, 5, 7, 6, 6, 5, 7, 7, 7, 6, 9, 9, + 7, 6, 6, 6, 6, 9, 6, 6, 6, 6, 7, 8, 7, 8, 9, 16, 14, 16, 16, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24 + }; +} diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java index 739704250..2ddb84315 100644 --- a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java @@ -39,7 +39,7 @@ public class HpackDraft05Test { private HpackDraft05.Reader hpackReader; @Before public void resetReader() { - hpackReader = new HpackDraft05.Reader(new DataInputStream(bytesIn)); + hpackReader = new HpackDraft05.Reader(false, new DataInputStream(bytesIn)); } /** @@ -171,12 +171,10 @@ public class HpackDraft05Test { hpackReader.emitReferenceSet(); assertEquals(1, hpackReader.headerCount); - // this will change when we decode huffman - assertEquals(48, hpackReader.headerTableByteCount); + assertEquals(52, hpackReader.headerTableByteCount); HpackDraft05.HeaderEntry entry = hpackReader.headerTable[headerTableLength() - 1]; - // TODO: huffman bytes are not what we want! - checkEntry(entry, ":path", new String(huffmanBytes, "UTF-8"), 48); + checkEntry(entry, ":path", "www.example.com", 52); assertHeaderReferenced(headerTableLength() - 1); } @@ -472,17 +470,237 @@ public class HpackDraft05Test { "custom-key", "custom-value"), hpackReader.getAndReset()); } + /** + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.3 + */ + @Test public void decodeRequestExamplesWithHuffman() throws IOException { + ByteArrayOutputStream out = firstRequestWithHuffman(); + bytesIn.set(out.toByteArray()); + hpackReader.readHeaders(out.size()); + hpackReader.emitReferenceSet(); + checkFirstRequestWithHuffman(); + + out = secondRequestWithHuffman(); + bytesIn.set(out.toByteArray()); + hpackReader.readHeaders(out.size()); + hpackReader.emitReferenceSet(); + checkSecondRequestWithHuffman(); + + out = thirdRequestWithHuffman(); + bytesIn.set(out.toByteArray()); + hpackReader.readHeaders(out.size()); + hpackReader.emitReferenceSet(); + checkThirdRequestWithHuffman(); + } + + private ByteArrayOutputStream firstRequestWithHuffman() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x82); // == Indexed - Add == + // idx = 2 -> :method: GET + out.write(0x87); // == Indexed - Add == + // idx = 7 -> :scheme: http + out.write(0x86); // == Indexed - Add == + // idx = 6 -> :path: / + out.write(0x04); // == Literal indexed == + // Indexed name (idx = 4) -> :authority + out.write(0x8b); // Literal value Huffman encoded 11 bytes + // decodes to www.example.com which is length 15 + byte[] huffmanBytes = new byte[] { + (byte) 0xdb, (byte) 0x6d, (byte) 0x88, (byte) 0x3e, + (byte) 0x68, (byte) 0xd1, (byte) 0xcb, (byte) 0x12, + (byte) 0x25, (byte) 0xba, (byte) 0x7f}; + out.write(huffmanBytes, 0, huffmanBytes.length); + + return out; + } + + private void checkFirstRequestWithHuffman() { + assertEquals(4, hpackReader.headerCount); + + // [ 1] (s = 57) :authority: www.example.com + HpackDraft05.HeaderEntry entry = hpackReader.headerTable[headerTableLength() - 4]; + checkEntry(entry, ":authority", "www.example.com", 57); + assertHeaderReferenced(headerTableLength() - 4); + + // [ 2] (s = 38) :path: / + entry = hpackReader.headerTable[headerTableLength() - 3]; + checkEntry(entry, ":path", "/", 38); + assertHeaderReferenced(headerTableLength() - 3); + + // [ 3] (s = 43) :scheme: http + entry = hpackReader.headerTable[headerTableLength() - 2]; + checkEntry(entry, ":scheme", "http", 43); + assertHeaderReferenced(headerTableLength() - 2); + + // [ 4] (s = 42) :method: GET + entry = hpackReader.headerTable[headerTableLength() - 1]; + checkEntry(entry, ":method", "GET", 42); + assertHeaderReferenced(headerTableLength() - 1); + + // Table size: 180 + assertEquals(180, hpackReader.headerTableByteCount); + + // Decoded header set: + assertEquals(byteStringList( + ":method", "GET", + ":scheme", "http", + ":path", "/", + ":authority", "www.example.com"), hpackReader.getAndReset()); + } + + private ByteArrayOutputStream secondRequestWithHuffman() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x1b); // == Literal indexed == + // Indexed name (idx = 27) -> cache-control + out.write(0x86); // Literal value Huffman encoded 6 bytes + // decodes to no-cache which is length 8 + byte[] huffmanBytes = new byte[] { + (byte) 0x63, (byte) 0x65, (byte) 0x4a, (byte) 0x13, + (byte) 0x98, (byte) 0xff}; + out.write(huffmanBytes, 0, huffmanBytes.length); + + return out; + } + + private void checkSecondRequestWithHuffman() { + assertEquals(5, hpackReader.headerCount); + + // [ 1] (s = 53) cache-control: no-cache + HpackDraft05.HeaderEntry entry = hpackReader.headerTable[headerTableLength() - 5]; + checkEntry(entry, "cache-control", "no-cache", 53); + assertHeaderReferenced(headerTableLength() - 5); + + // [ 2] (s = 57) :authority: www.example.com + entry = hpackReader.headerTable[headerTableLength() - 4]; + checkEntry(entry, ":authority", "www.example.com", 57); + assertHeaderReferenced(headerTableLength() - 4); + + // [ 3] (s = 38) :path: / + entry = hpackReader.headerTable[headerTableLength() - 3]; + checkEntry(entry, ":path", "/", 38); + assertHeaderReferenced(headerTableLength() - 3); + + // [ 4] (s = 43) :scheme: http + entry = hpackReader.headerTable[headerTableLength() - 2]; + checkEntry(entry, ":scheme", "http", 43); + assertHeaderReferenced(headerTableLength() - 2); + + // [ 5] (s = 42) :method: GET + entry = hpackReader.headerTable[headerTableLength() - 1]; + checkEntry(entry, ":method", "GET", 42); + assertHeaderReferenced(headerTableLength() - 1); + + // Table size: 233 + assertEquals(233, hpackReader.headerTableByteCount); + + // Decoded header set: + assertEquals(byteStringList( + ":method", "GET", + ":scheme", "http", + ":path", "/", + ":authority", "www.example.com", + "cache-control", "no-cache"), hpackReader.getAndReset()); + } + + private ByteArrayOutputStream thirdRequestWithHuffman() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x80); // == Empty reference set == + out.write(0x85); // == Indexed - Add == + // idx = 5 -> :method: GET + out.write(0x8c); // == Indexed - Add == + // idx = 12 -> :scheme: https + out.write(0x8b); // == Indexed - Add == + // idx = 11 -> :path: /index.html + out.write(0x84); // == Indexed - Add == + // idx = 4 -> :authority: www.example.com + out.write(0x00); // Literal indexed + out.write(0x88); // Literal name Huffman encoded 8 bytes + // decodes to custom-key which is length 10 + byte[] huffmanBytes = new byte[] { + (byte) 0x4e, (byte) 0xb0, (byte) 0x8b, (byte) 0x74, + (byte) 0x97, (byte) 0x90, (byte) 0xfa, (byte) 0x7f}; + out.write(huffmanBytes, 0, huffmanBytes.length); + out.write(0x89); // Literal value Huffman encoded 6 bytes + // decodes to custom-value which is length 12 + huffmanBytes = new byte[] { + (byte) 0x4e, (byte) 0xb0, (byte) 0x8b, (byte) 0x74, + (byte) 0x97, (byte) 0x9a, (byte) 0x17, (byte) 0xa8, + (byte) 0xff}; + out.write(huffmanBytes, 0, huffmanBytes.length); + + return out; + } + + private void checkThirdRequestWithHuffman() { + assertEquals(8, hpackReader.headerCount); + + // [ 1] (s = 54) custom-key: custom-value + HpackDraft05.HeaderEntry entry = hpackReader.headerTable[headerTableLength() - 8]; + checkEntry(entry, "custom-key", "custom-value", 54); + assertHeaderReferenced(headerTableLength() - 8); + + // [ 2] (s = 48) :path: /index.html + entry = hpackReader.headerTable[headerTableLength() - 7]; + checkEntry(entry, ":path", "/index.html", 48); + assertHeaderReferenced(headerTableLength() - 7); + + // [ 3] (s = 44) :scheme: https + entry = hpackReader.headerTable[headerTableLength() - 6]; + checkEntry(entry, ":scheme", "https", 44); + assertHeaderReferenced(headerTableLength() - 6); + + // [ 4] (s = 53) cache-control: no-cache + entry = hpackReader.headerTable[headerTableLength() - 5]; + checkEntry(entry, "cache-control", "no-cache", 53); + assertHeaderNotReferenced(headerTableLength() - 5); + + // [ 5] (s = 57) :authority: www.example.com + entry = hpackReader.headerTable[headerTableLength() - 4]; + checkEntry(entry, ":authority", "www.example.com", 57); + assertHeaderReferenced(headerTableLength() - 4); + + // [ 6] (s = 38) :path: / + entry = hpackReader.headerTable[headerTableLength() - 3]; + checkEntry(entry, ":path", "/", 38); + assertHeaderNotReferenced(headerTableLength() - 3); + + // [ 7] (s = 43) :scheme: http + entry = hpackReader.headerTable[headerTableLength() - 2]; + checkEntry(entry, ":scheme", "http", 43); + assertHeaderNotReferenced(headerTableLength() - 2); + + // [ 8] (s = 42) :method: GET + entry = hpackReader.headerTable[headerTableLength() - 1]; + checkEntry(entry, ":method", "GET", 42); + assertHeaderReferenced(headerTableLength() - 1); + + // Table size: 379 + assertEquals(379, hpackReader.headerTableByteCount); + + // Decoded header set: + // TODO: order is not correct per docs, but then again, the spec doesn't require ordering. + assertEquals(byteStringList( + ":method", "GET", + ":authority", "www.example.com", + ":scheme", "https", + ":path", "/index.html", + "custom-key", "custom-value"), hpackReader.getAndReset()); + } + private ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); private final HpackDraft05.Writer hpackWriter = new HpackDraft05.Writer(new DataOutputStream(bytesOut)); @Test public void readSingleByteInt() throws IOException { - assertEquals(10, new HpackDraft05.Reader(byteStream()).readInt(10, 31)); - assertEquals(10, new HpackDraft05.Reader(byteStream()).readInt(0xe0 | 10, 31)); + assertEquals(10, new HpackDraft05.Reader(false, byteStream()).readInt(10, 31)); + assertEquals(10, new HpackDraft05.Reader(false, byteStream()).readInt(0xe0 | 10, 31)); } @Test public void readMultibyteInt() throws IOException { - assertEquals(1337, new HpackDraft05.Reader(byteStream(154, 10)).readInt(31, 31)); + assertEquals(1337, new HpackDraft05.Reader(false, byteStream(154, 10)).readInt(31, 31)); } @Test public void writeSingleByteInt() throws IOException { @@ -503,44 +721,44 @@ public class HpackDraft05Test { hpackWriter.writeInt(0x7fffffff, 31, 0); assertBytes(31, 224, 255, 255, 255, 7); assertEquals(0x7fffffff, - new HpackDraft05.Reader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31)); + new HpackDraft05.Reader(false, 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, new HpackDraft05.Reader(byteStream(0)).readInt(31, 31)); + assertEquals(31, new HpackDraft05.Reader(false, byteStream(0)).readInt(31, 31)); } @Test public void prefixMaskMinusOne() throws IOException { hpackWriter.writeInt(30, 31, 0); assertBytes(30); - assertEquals(31, new HpackDraft05.Reader(byteStream(0)).readInt(31, 31)); + assertEquals(31, new HpackDraft05.Reader(false, byteStream(0)).readInt(31, 31)); } @Test public void zero() throws IOException { hpackWriter.writeInt(0, 31, 0); assertBytes(0); - assertEquals(0, new HpackDraft05.Reader(byteStream()).readInt(0, 31)); + assertEquals(0, new HpackDraft05.Reader(false, byteStream()).readInt(0, 31)); } @Test public void headerName() throws IOException { hpackWriter.writeByteString(ByteString.encodeUtf8("foo")); assertBytes(3, 'f', 'o', 'o'); - assertEquals("foo", new HpackDraft05.Reader(byteStream(3, 'f', 'o', 'o')).readString().utf8()); + assertEquals("foo", new HpackDraft05.Reader(false, byteStream(3, 'f', 'o', 'o')).readString().utf8()); } @Test public void emptyHeaderName() throws IOException { hpackWriter.writeByteString(ByteString.encodeUtf8("")); assertBytes(0); - assertEquals("", new HpackDraft05.Reader(byteStream(0)).readString().utf8()); + assertEquals("", new HpackDraft05.Reader(false, byteStream(0)).readString().utf8()); } @Test public void headersRoundTrip() throws IOException { List sentHeaders = byteStringList("name", "value"); hpackWriter.writeHeaders(sentHeaders); ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesOut.toByteArray()); - HpackDraft05.Reader reader = new HpackDraft05.Reader(new DataInputStream(bytesIn)); + HpackDraft05.Reader reader = new HpackDraft05.Reader(false, new DataInputStream(bytesIn)); reader.readHeaders(bytesOut.size()); reader.emitReferenceSet(); List receivedHeaders = reader.getAndReset(); diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java new file mode 100644 index 000000000..6206b7efb --- /dev/null +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 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 com.squareup.okhttp.internal.spdy; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Original version of this class was lifted from {@code com.twitter.hpack.HuffmanTest}. + */ +public class HuffmanTest { + + @Test public void roundTripForRequestAndResponse() throws IOException { + String s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (int i = 0; i < s.length(); i++) { + assertRoundTrip(s.substring(0, i).getBytes()); + } + + Random random = new Random(123456789L); + byte[] buf = new byte[4096]; + random.nextBytes(buf); + assertRoundTrip(buf); + } + + private void assertRoundTrip(byte[] buf) throws IOException { + assertRoundTrip(Huffman.Codec.REQUEST, buf); + assertRoundTrip(Huffman.Codec.RESPONSE, buf); + } + + private static void assertRoundTrip(Huffman.Codec codec, byte[] buf) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + + codec.encode(buf, dos); + assertEquals(baos.size(), codec.encodedLength(buf)); + + byte[] decodedBytes = codec.decode(baos.toByteArray()); + assertTrue(Arrays.equals(buf, decodedBytes)); + } +} diff --git a/okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java b/okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java index 483f3cd96..36d54668e 100644 --- a/okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java +++ b/okhttp/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java @@ -38,6 +38,7 @@ public final class ExternalHttp2Example { int responseCode = connection.getResponseCode(); System.out.println(responseCode); + System.out.println(connection.getHeaderFields()); BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));