From 1b0e8a4e73ab119238a4b7507ff55015f1ca5255 Mon Sep 17 00:00:00 2001 From: Pavel Rappo <prappo@openjdk.org> Date: Mon, 18 Apr 2016 19:40:48 +0100 Subject: [PATCH] 8153353: HPACK implementation Reviewed-by: chegar, rriggs --- .../hpack/BinaryRepresentationWriter.java | 34 + .../hpack/BulkSizeUpdateWriter.java | 78 ++ .../sun/net/httpclient/hpack/Decoder.java | 506 +++++++++++++ .../httpclient/hpack/DecodingCallback.java | 284 ++++++++ .../sun/net/httpclient/hpack/Encoder.java | 429 +++++++++++ .../sun/net/httpclient/hpack/HeaderTable.java | 511 +++++++++++++ .../sun/net/httpclient/hpack/Huffman.java | 676 ++++++++++++++++++ .../sun/net/httpclient/hpack/ISO_8859_1.java | 103 +++ .../hpack/IndexNameValueWriter.java | 101 +++ .../net/httpclient/hpack/IndexedWriter.java | 50 ++ .../net/httpclient/hpack/IntegerReader.java | 142 ++++ .../net/httpclient/hpack/IntegerWriter.java | 117 +++ .../hpack/LiteralNeverIndexedWriter.java | 32 + .../hpack/LiteralWithIndexingWriter.java | 85 +++ .../net/httpclient/hpack/LiteralWriter.java | 32 + .../httpclient/hpack/SizeUpdateWriter.java | 59 ++ .../net/httpclient/hpack/StringReader.java | 111 +++ .../net/httpclient/hpack/StringWriter.java | 126 ++++ .../net/httpclient/hpack/package-info.java | 34 + .../net/httpclient/http2/HpackDriver.java | 40 ++ .../hpack/BinaryPrimitivesTest.java | 347 +++++++++ .../httpclient/hpack/BuffersTestingKit.java | 210 ++++++ .../httpclient/hpack/CircularBufferTest.java | 125 ++++ .../sun/net/httpclient/hpack/DecoderTest.java | 595 +++++++++++++++ .../sun/net/httpclient/hpack/EncoderTest.java | 623 ++++++++++++++++ .../net/httpclient/hpack/HeaderTableTest.java | 375 ++++++++++ .../sun/net/httpclient/hpack/HuffmanTest.java | 623 ++++++++++++++++ .../sun/net/httpclient/hpack/SpecHelper.java | 70 ++ .../sun/net/httpclient/hpack/TestHelper.java | 164 +++++ 29 files changed, 6682 insertions(+) create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/BinaryRepresentationWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/BulkSizeUpdateWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Decoder.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/DecodingCallback.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Encoder.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/HeaderTable.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Huffman.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/ISO_8859_1.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IndexNameValueWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IndexedWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IntegerReader.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IntegerWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralNeverIndexedWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralWithIndexingWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/SizeUpdateWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/StringReader.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/StringWriter.java create mode 100644 jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/package-info.java create mode 100644 jdk/test/java/net/httpclient/http2/HpackDriver.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/BinaryPrimitivesTest.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/BuffersTestingKit.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/CircularBufferTest.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/DecoderTest.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/EncoderTest.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/HeaderTableTest.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/HuffmanTest.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/SpecHelper.java create mode 100644 jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/TestHelper.java diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/BinaryRepresentationWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/BinaryRepresentationWriter.java new file mode 100644 index 00000000000..de60d58f6ac --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/BinaryRepresentationWriter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; + +interface BinaryRepresentationWriter { + + boolean write(HeaderTable table, ByteBuffer destination); + + BinaryRepresentationWriter reset(); +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/BulkSizeUpdateWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/BulkSizeUpdateWriter.java new file mode 100644 index 00000000000..1c064f627f5 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/BulkSizeUpdateWriter.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; +import java.util.Iterator; + +import static java.util.Objects.requireNonNull; + +final class BulkSizeUpdateWriter implements BinaryRepresentationWriter { + + private final SizeUpdateWriter writer = new SizeUpdateWriter(); + private Iterator<Integer> maxSizes; + private boolean writing; + private boolean configured; + + BulkSizeUpdateWriter maxHeaderTableSizes(Iterable<Integer> sizes) { + if (configured) { + throw new IllegalStateException("Already configured"); + } + requireNonNull(sizes, "sizes"); + maxSizes = sizes.iterator(); + configured = true; + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + if (!configured) { + throw new IllegalStateException("Configure first"); + } + while (true) { + if (writing) { + if (!writer.write(table, destination)) { + return false; + } + writing = false; + } else if (maxSizes.hasNext()) { + writing = true; + writer.reset(); + writer.maxHeaderTableSize(maxSizes.next()); + } else { + configured = false; + return true; + } + } + } + + @Override + public BulkSizeUpdateWriter reset() { + maxSizes = null; + writing = false; + configured = false; + return this; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Decoder.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Decoder.java new file mode 100644 index 00000000000..afff906424a --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Decoder.java @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +/** + * Decodes headers from their binary representation. + * + * <p>Typical lifecycle looks like this: + * + * <p> {@link #Decoder(int) new Decoder} + * ({@link #setMaxCapacity(int) setMaxCapacity}? + * {@link #decode(ByteBuffer, boolean, DecodingCallback) decode})* + * + * @apiNote + * + * <p> The design intentions behind Decoder were to facilitate flexible and + * incremental style of processing. + * + * <p> {@code Decoder} does not require a complete header block in a single + * {@code ByteBuffer}. The header block can be spread across many buffers of any + * size and decoded one-by-one the way it makes most sense for the user. This + * way also allows not to limit the size of the header block. + * + * <p> Headers are delivered to the {@linkplain DecodingCallback callback} as + * soon as they become decoded. Using the callback also gives the user a freedom + * to decide how headers are processed. The callback does not limit the number + * of headers decoded during single decoding operation. + * + * @since 9 + */ +public final class Decoder { + + private static final State[] states = new State[256]; + + static { + // To be able to do a quick lookup, each of 256 possibilities are mapped + // to corresponding states. + // + // We can safely do this since patterns 1, 01, 001, 0001, 0000 are + // Huffman prefixes and therefore are inherently not ambiguous. + // + // I do it mainly for better debugging (to not go each time step by step + // through if...else tree). As for performance win for the decoding, I + // believe is negligible. + for (int i = 0; i < states.length; i++) { + if ((i & 0b1000_0000) == 0b1000_0000) { + states[i] = State.INDEXED; + } else if ((i & 0b1100_0000) == 0b0100_0000) { + states[i] = State.LITERAL_WITH_INDEXING; + } else if ((i & 0b1110_0000) == 0b0010_0000) { + states[i] = State.SIZE_UPDATE; + } else if ((i & 0b1111_0000) == 0b0001_0000) { + states[i] = State.LITERAL_NEVER_INDEXED; + } else if ((i & 0b1111_0000) == 0b0000_0000) { + states[i] = State.LITERAL; + } else { + throw new InternalError(String.valueOf(i)); + } + } + } + + private final HeaderTable table; + + private State state = State.READY; + private final IntegerReader integerReader; + private final StringReader stringReader; + private final StringBuilder name; + private final StringBuilder value; + private int intValue; + private boolean firstValueRead; + private boolean firstValueIndex; + private boolean nameHuffmanEncoded; + private boolean valueHuffmanEncoded; + private int capacity; + + /** + * Constructs a {@code Decoder} with the specified initial capacity of the + * header table. + * + * <p> The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses HPACK (see <a + * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table + * Size</a>). + * + * @param capacity + * a non-negative integer + * + * @throws IllegalArgumentException + * if capacity is negative + */ + public Decoder(int capacity) { + setMaxCapacity(capacity); + table = new HeaderTable(capacity); + integerReader = new IntegerReader(); + stringReader = new StringReader(); + name = new StringBuilder(512); + value = new StringBuilder(1024); + } + + /** + * Sets a maximum capacity of the header table. + * + * <p> The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses HPACK (see <a + * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table + * Size</a>). + * + * @param capacity + * a non-negative integer + * + * @throws IllegalArgumentException + * if capacity is negative + */ + public void setMaxCapacity(int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException("capacity >= 0: " + capacity); + } + // FIXME: await capacity update if less than what was prior to it + this.capacity = capacity; + } + + /** + * Decodes a header block from the given buffer to the given callback. + * + * <p> Suppose a header block is represented by a sequence of {@code + * ByteBuffer}s in the form of {@code Iterator<ByteBuffer>}. And the + * consumer of decoded headers is represented by the callback. Then to + * decode the header block, the following approach might be used: + * + * <pre>{@code + * while (buffers.hasNext()) { + * ByteBuffer input = buffers.next(); + * decoder.decode(input, callback, !buffers.hasNext()); + * } + * }</pre> + * + * <p> The decoder reads as much as possible of the header block from the + * given buffer, starting at the buffer's position, and increments its + * position to reflect the bytes read. The buffer's mark and limit will not + * be modified. + * + * <p> Once the method is invoked with {@code endOfHeaderBlock == true}, the + * current header block is deemed ended, and inconsistencies, if any, are + * reported immediately by throwing an {@code UncheckedIOException}. + * + * <p> Each callback method is called only after the implementation has + * processed the corresponding bytes. If the bytes revealed a decoding + * error, the callback method is not called. + * + * <p> In addition to exceptions thrown directly by the method, any + * exceptions thrown from the {@code callback} will bubble up. + * + * @apiNote The method asks for {@code endOfHeaderBlock} flag instead of + * returning it for two reasons. The first one is that the user of the + * decoder always knows which chunk is the last. The second one is to throw + * the most detailed exception possible, which might be useful for + * diagnosing issues. + * + * @implNote This implementation is not atomic in respect to decoding + * errors. In other words, if the decoding operation has thrown a decoding + * error, the decoder is no longer usable. + * + * @param headerBlock + * the chunk of the header block, may be empty + * @param endOfHeaderBlock + * true if the chunk is the final (or the only one) in the sequence + * + * @param consumer + * the callback + * @throws UncheckedIOException + * in case of a decoding error + * @throws NullPointerException + * if either headerBlock or consumer are null + */ + public void decode(ByteBuffer headerBlock, boolean endOfHeaderBlock, + DecodingCallback consumer) { + requireNonNull(headerBlock, "headerBlock"); + requireNonNull(consumer, "consumer"); + while (headerBlock.hasRemaining()) { + proceed(headerBlock, consumer); + } + if (endOfHeaderBlock && state != State.READY) { + throw new UncheckedIOException( + new ProtocolException("Unexpected end of header block")); + } + } + + private void proceed(ByteBuffer input, DecodingCallback action) { + switch (state) { + case READY: + resumeReady(input); + break; + case INDEXED: + resumeIndexed(input, action); + break; + case LITERAL: + resumeLiteral(input, action); + break; + case LITERAL_WITH_INDEXING: + resumeLiteralWithIndexing(input, action); + break; + case LITERAL_NEVER_INDEXED: + resumeLiteralNeverIndexed(input, action); + break; + case SIZE_UPDATE: + resumeSizeUpdate(input, action); + break; + default: + throw new InternalError( + "Unexpected decoder state: " + String.valueOf(state)); + } + } + + private void resumeReady(ByteBuffer input) { + int b = input.get(input.position()) & 0xff; // absolute read + State s = states[b]; + switch (s) { + case INDEXED: + integerReader.configure(7); + state = State.INDEXED; + firstValueIndex = true; + break; + case LITERAL: + state = State.LITERAL; + firstValueIndex = (b & 0b0000_1111) != 0; + if (firstValueIndex) { + integerReader.configure(4); + } + break; + case LITERAL_WITH_INDEXING: + state = State.LITERAL_WITH_INDEXING; + firstValueIndex = (b & 0b0011_1111) != 0; + if (firstValueIndex) { + integerReader.configure(6); + } + break; + case LITERAL_NEVER_INDEXED: + state = State.LITERAL_NEVER_INDEXED; + firstValueIndex = (b & 0b0000_1111) != 0; + if (firstValueIndex) { + integerReader.configure(4); + } + break; + case SIZE_UPDATE: + integerReader.configure(5); + state = State.SIZE_UPDATE; + firstValueIndex = true; + break; + default: + throw new InternalError(String.valueOf(s)); + } + if (!firstValueIndex) { + input.get(); // advance, next stop: "String Literal" + } + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | Index (7+) | + // +---+---------------------------+ + // + private void resumeIndexed(ByteBuffer input, DecodingCallback action) { + if (!integerReader.read(input)) { + return; + } + intValue = integerReader.get(); + integerReader.reset(); + try { + HeaderTable.HeaderField f = table.get(intValue); + action.onIndexed(intValue, f.name, f.value); + } finally { + state = State.READY; + } + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | Index (4+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + private void resumeLiteral(ByteBuffer input, DecodingCallback action) { + if (!completeReading(input)) { + return; + } + try { + if (firstValueIndex) { + HeaderTable.HeaderField f = table.get(intValue); + action.onLiteral(intValue, f.name, value, valueHuffmanEncoded); + } else { + action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded); + } + } finally { + cleanUpAfterReading(); + } + } + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Index (6+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + private void resumeLiteralWithIndexing(ByteBuffer input, DecodingCallback action) { + if (!completeReading(input)) { + return; + } + try { + // + // 1. (name, value) will be stored in the table as strings + // 2. Most likely the callback will also create strings from them + // ------------------------------------------------------------------------ + // Let's create those string beforehand (and only once!) to benefit everyone + // + String n; + String v = value.toString(); + if (firstValueIndex) { + HeaderTable.HeaderField f = table.get(intValue); + n = f.name; + action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded); + } else { + n = name.toString(); + action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded); + } + table.put(n, v); + } catch (IllegalArgumentException | IllegalStateException e) { + throw new UncheckedIOException( + (IOException) new ProtocolException().initCause(e)); + } finally { + cleanUpAfterReading(); + } + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + // + private void resumeLiteralNeverIndexed(ByteBuffer input, DecodingCallback action) { + if (!completeReading(input)) { + return; + } + try { + if (firstValueIndex) { + HeaderTable.HeaderField f = table.get(intValue); + action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded); + } else { + action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded); + } + } finally { + cleanUpAfterReading(); + } + } + + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Max size (5+) | + // +---+---------------------------+ + // + private void resumeSizeUpdate(ByteBuffer input, DecodingCallback action) { + if (!integerReader.read(input)) { + return; + } + intValue = integerReader.get(); + assert intValue >= 0; + if (intValue > capacity) { + throw new UncheckedIOException(new ProtocolException( + format("Received capacity exceeds expected: " + + "capacity=%s, expected=%s", intValue, capacity))); + } + integerReader.reset(); + try { + action.onSizeUpdate(intValue); + table.setMaxSize(intValue); + } finally { + state = State.READY; + } + } + + private boolean completeReading(ByteBuffer input) { + if (!firstValueRead) { + if (firstValueIndex) { + if (!integerReader.read(input)) { + return false; + } + intValue = integerReader.get(); + integerReader.reset(); + } else { + if (!stringReader.read(input, name)) { + return false; + } + nameHuffmanEncoded = stringReader.isHuffmanEncoded(); + stringReader.reset(); + } + firstValueRead = true; + return false; + } else { + if (!stringReader.read(input, value)) { + return false; + } + } + valueHuffmanEncoded = stringReader.isHuffmanEncoded(); + stringReader.reset(); + return true; + } + + private void cleanUpAfterReading() { + name.setLength(0); + value.setLength(0); + firstValueRead = false; + state = State.READY; + } + + private enum State { + READY, + INDEXED, + LITERAL_NEVER_INDEXED, + LITERAL, + LITERAL_WITH_INDEXING, + SIZE_UPDATE + } + + HeaderTable getTable() { + return table; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/DecodingCallback.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/DecodingCallback.java new file mode 100644 index 00000000000..d0d1ce5d5d5 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/DecodingCallback.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; + +/** + * Delivers results of the {@link Decoder#decode(ByteBuffer, boolean, + * DecodingCallback) decoding operation}. + * + * <p> Methods of the callback are never called by a decoder with any of the + * arguments being {@code null}. + * + * @apiNote + * + * <p> The callback provides methods for all possible <a + * href="https://tools.ietf.org/html/rfc7541#section-6">binary + * representations</a>. This could be useful for implementing an intermediary, + * logging, debugging, etc. + * + * <p> The callback is an interface in order to interoperate with lambdas (in + * the most common use case): + * <pre>{@code + * DecodingCallback callback = (name, value) -> System.out.println(name + ", " + value); + * }</pre> + * + * <p> Names and values are {@link CharSequence}s rather than {@link String}s in + * order to allow users to decide whether or not they need to create objects. A + * {@code CharSequence} might be used in-place, for example, to be appended to + * an {@link Appendable} (e.g. {@link StringBuilder}) and then discarded. + * + * <p> That said, if a passed {@code CharSequence} needs to outlast the method + * call, it needs to be copied. + * + * @since 9 + */ +@FunctionalInterface +public interface DecodingCallback { + + /** + * A method the more specific methods of the callback forward their calls + * to. + * + * @param name + * header name + * @param value + * header value + */ + void onDecoded(CharSequence name, CharSequence value); + + /** + * A more finer-grained version of {@link #onDecoded(CharSequence, + * CharSequence)} that also reports on value sensitivity. + * + * <p> Value sensitivity must be considered, for example, when implementing + * an intermediary. A {@code value} is sensitive if it was represented as <a + * href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal Header + * Field Never Indexed</a>. + * + * <p> It is required that intermediaries MUST use the {@linkplain + * Encoder#header(CharSequence, CharSequence, boolean) same representation} + * for encoding this header field in order to protect its value which is not + * to be put at risk by compressing it. + * + * @implSpec + * + * <p> The default implementation invokes {@code onDecoded(name, value)}. + * + * @param name + * header name + * @param value + * header value + * @param sensitive + * whether or not the value is sensitive + * + * @see #onLiteralNeverIndexed(int, CharSequence, CharSequence, boolean) + * @see #onLiteralNeverIndexed(CharSequence, boolean, CharSequence, boolean) + */ + default void onDecoded(CharSequence name, CharSequence value, + boolean sensitive) { + onDecoded(name, value); + } + + /** + * An <a href="https://tools.ietf.org/html/rfc7541#section-6.1">Indexed + * Header Field</a> decoded. + * + * @implSpec + * + * <p> The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param name + * header name + * @param value + * header value + */ + default void onIndexed(int index, CharSequence name, CharSequence value) { + onDecoded(name, value, false); + } + + /** + * A <a href="https://tools.ietf.org/html/rfc7541#section-6.2.2">Literal + * Header Field without Indexing</a> decoded, where a {@code name} was + * referred by an {@code index}. + * + * @implSpec + * + * <p> The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param name + * header name + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteral(int index, CharSequence name, + CharSequence value, boolean valueHuffman) { + onDecoded(name, value, false); + } + + /** + * A <a href="https://tools.ietf.org/html/rfc7541#section-6.2.2">Literal + * Header Field without Indexing</a> decoded, where both a {@code name} and + * a {@code value} were literal. + * + * @implSpec + * + * <p> The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteral(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman) { + onDecoded(name, value, false); + } + + /** + * A <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal + * Header Field Never Indexed</a> decoded, where a {@code name} + * was referred by an {@code index}. + * + * @implSpec + * + * <p> The default implementation invokes + * {@code onDecoded(name, value, true)}. + * + * @param index + * index of an entry in the table + * @param name + * header name + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralNeverIndexed(int index, CharSequence name, + CharSequence value, + boolean valueHuffman) { + onDecoded(name, value, true); + } + + /** + * A <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">Literal + * Header Field Never Indexed</a> decoded, where both a {@code + * name} and a {@code value} were literal. + * + * @implSpec + * + * <p> The default implementation invokes + * {@code onDecoded(name, value, true)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralNeverIndexed(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman) { + onDecoded(name, value, true); + } + + /** + * A <a href="https://tools.ietf.org/html/rfc7541#section-6.2.1">Literal + * Header Field with Incremental Indexing</a> decoded, where a {@code name} + * was referred by an {@code index}. + * + * @implSpec + * + * <p> The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param index + * index of an entry in the table + * @param name + * header name + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralWithIndexing(int index, + CharSequence name, + CharSequence value, boolean valueHuffman) { + onDecoded(name, value, false); + } + + /** + * A <a href="https://tools.ietf.org/html/rfc7541#section-6.2.1">Literal + * Header Field with Incremental Indexing</a> decoded, where both a {@code + * name} and a {@code value} were literal. + * + * @implSpec + * + * <p> The default implementation invokes + * {@code onDecoded(name, value, false)}. + * + * @param name + * header name + * @param nameHuffman + * if the {@code name} was Huffman encoded + * @param value + * header value + * @param valueHuffman + * if the {@code value} was Huffman encoded + */ + default void onLiteralWithIndexing(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman) { + onDecoded(name, value, false); + } + + /** + * A <a href="https://tools.ietf.org/html/rfc7541#section-6.3">Dynamic Table + * Size Update</a> decoded. + * + * @implSpec + * + * <p> The default implementation does nothing. + * + * @param capacity + * new capacity of the header table + */ + default void onSizeUpdate(int capacity) { } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Encoder.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Encoder.java new file mode 100644 index 00000000000..75ab8653457 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Encoder.java @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.util.LinkedList; +import java.util.List; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +/** + * Encodes headers to their binary representation. + * + * <p>Typical lifecycle looks like this: + * + * <p> {@link #Encoder(int) new Encoder} + * ({@link #setMaxCapacity(int) setMaxCapacity}? + * {@link #encode(ByteBuffer) encode})* + * + * <p> Suppose headers are represented by {@code Map<String, List<String>>}. A + * supplier and a consumer of {@link ByteBuffer}s in forms of {@code + * Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively. Then to + * encode headers, the following approach might be used: + * + * <pre>{@code + * for (Map.Entry<String, List<String>> h : headers.entrySet()) { + * String name = h.getKey(); + * for (String value : h.getValue()) { + * encoder.header(name, value); // Set up header + * boolean encoded; + * do { + * ByteBuffer b = buffersSupplier.get(); + * encoded = encoder.encode(b); // Encode the header + * buffersConsumer.accept(b); + * } while (!encoded); + * } + * } + * }</pre> + * + * <p> Though the specification <a + * href="https://tools.ietf.org/html/rfc7541#section-2"> does not define</a> how + * an encoder is to be implemented, a default implementation is provided by the + * method {@link #header(CharSequence, CharSequence, boolean)}. + * + * <p> To provide a custom encoding implementation, {@code Encoder} has to be + * extended. A subclass then can access methods for encoding using specific + * representations (e.g. {@link #literal(int, CharSequence, boolean) literal}, + * {@link #indexed(int) indexed}, etc.) + * + * @apiNote + * + * <p> An Encoder provides an incremental way of encoding headers. + * {@link #encode(ByteBuffer)} takes a buffer a returns a boolean indicating + * whether, or not, the buffer was sufficiently sized to hold the + * remaining of the encoded representation. + * + * <p> This way, there's no need to provide a buffer of a specific size, or to + * resize (and copy) the buffer on demand, when the remaining encoded + * representation will not fit in the buffer's remaining space. Instead, an + * array of existing buffers can be used, prepended with a frame that encloses + * the resulting header block afterwards. + * + * <p> Splitting the encoding operation into header set up and header encoding, + * separates long lived arguments ({@code name}, {@code value}, {@code + * sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}), + * simplifying each operation itself. + * + * @implNote + * + * <p> The default implementation does not use dynamic table. It reports to a + * coupled Decoder a size update with the value of {@code 0}, and never changes + * it afterwards. + * + * @since 9 + */ +public class Encoder { + + // TODO: enum: no huffman/smart huffman/always huffman + private static final boolean DEFAULT_HUFFMAN = true; + + private final IndexedWriter indexedWriter = new IndexedWriter(); + private final LiteralWriter literalWriter = new LiteralWriter(); + private final LiteralNeverIndexedWriter literalNeverIndexedWriter + = new LiteralNeverIndexedWriter(); + private final LiteralWithIndexingWriter literalWithIndexingWriter + = new LiteralWithIndexingWriter(); + private final SizeUpdateWriter sizeUpdateWriter = new SizeUpdateWriter(); + private final BulkSizeUpdateWriter bulkSizeUpdateWriter + = new BulkSizeUpdateWriter(); + + private BinaryRepresentationWriter writer; + private final HeaderTable headerTable; + + private boolean encoding; + + private int maxCapacity; + private int currCapacity; + private int lastCapacity; + private long minCapacity; + private boolean capacityUpdate; + private boolean configuredCapacityUpdate; + + /** + * Constructs an {@code Encoder} with the specified maximum capacity of the + * header table. + * + * <p> The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses HPACK (see <a + * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table + * Size</a>). + * + * @param maxCapacity + * a non-negative integer + * + * @throws IllegalArgumentException + * if maxCapacity is negative + */ + public Encoder(int maxCapacity) { + if (maxCapacity < 0) { + throw new IllegalArgumentException("maxCapacity >= 0: " + maxCapacity); + } + // Initial maximum capacity update mechanics + minCapacity = Long.MAX_VALUE; + currCapacity = -1; + setMaxCapacity(maxCapacity); + headerTable = new HeaderTable(lastCapacity); + } + + /** + * Sets up the given header {@code (name, value)}. + * + * <p> Fixates {@code name} and {@code value} for the duration of encoding. + * + * @param name + * the name + * @param value + * the value + * + * @throws NullPointerException + * if any of the arguments are {@code null} + * @throws IllegalStateException + * if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + * @see #header(CharSequence, CharSequence, boolean) + */ + public void header(CharSequence name, CharSequence value) + throws IllegalStateException { + header(name, value, false); + } + + /** + * Sets up the given header {@code (name, value)} with possibly sensitive + * value. + * + * <p> Fixates {@code name} and {@code value} for the duration of encoding. + * + * @param name + * the name + * @param value + * the value + * @param sensitive + * whether or not the value is sensitive + * + * @throws NullPointerException + * if any of the arguments are {@code null} + * @throws IllegalStateException + * if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + * @see #header(CharSequence, CharSequence) + * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean) + */ + public void header(CharSequence name, CharSequence value, + boolean sensitive) throws IllegalStateException { + // Arguably a good balance between complexity of implementation and + // efficiency of encoding + requireNonNull(name, "name"); + requireNonNull(value, "value"); + HeaderTable t = getHeaderTable(); + int index = t.indexOf(name, value); + if (index > 0) { + indexed(index); + } else if (index < 0) { + if (sensitive) { + literalNeverIndexed(-index, value, DEFAULT_HUFFMAN); + } else { + literal(-index, value, DEFAULT_HUFFMAN); + } + } else { + if (sensitive) { + literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); + } else { + literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN); + } + } + } + + /** + * Sets a maximum capacity of the header table. + * + * <p> The value has to be agreed between decoder and encoder out-of-band, + * e.g. by a protocol that uses HPACK (see <a + * href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table + * Size</a>). + * + * <p> May be called any number of times after or before a complete header + * has been encoded. + * + * <p> If the encoder decides to change the actual capacity, an update will + * be encoded before a new encoding operation starts. + * + * @param capacity + * a non-negative integer + * + * @throws IllegalArgumentException + * if capacity is negative + * @throws IllegalStateException + * if the encoder hasn't fully encoded the previous header, or + * hasn't yet started to encode it + */ + public void setMaxCapacity(int capacity) { + checkEncoding(); + if (capacity < 0) { + throw new IllegalArgumentException("capacity >= 0: " + capacity); + } + int calculated = calculateCapacity(capacity); + if (calculated < 0 || calculated > capacity) { + throw new IllegalArgumentException( + format("0 <= calculated <= capacity: calculated=%s, capacity=%s", + calculated, capacity)); + } + capacityUpdate = true; + // maxCapacity needs to be updated unconditionally, so the encoder + // always has the newest one (in case it decides to update it later + // unsolicitedly) + // Suppose maxCapacity = 4096, and the encoder has decided to use only + // 2048. It later can choose anything else from the region [0, 4096]. + maxCapacity = capacity; + lastCapacity = calculated; + minCapacity = Math.min(minCapacity, lastCapacity); + } + + protected int calculateCapacity(int maxCapacity) { + // Default implementation of the Encoder won't add anything to the + // table, therefore no need for a table space + return 0; + } + + /** + * Encodes the {@linkplain #header(CharSequence, CharSequence) set up} + * header into the given buffer. + * + * <p> The encoder writes as much as possible of the header's binary + * representation into the given buffer, starting at the buffer's position, + * and increments its position to reflect the bytes written. The buffer's + * mark and limit will not be modified. + * + * <p> Once the method has returned {@code true}, the current header is + * deemed encoded. A new header may be set up. + * + * @param headerBlock + * the buffer to encode the header into, may be empty + * + * @return {@code true} if the current header has been fully encoded, + * {@code false} otherwise + * + * @throws NullPointerException + * if the buffer is {@code null} + * @throws ReadOnlyBufferException + * if this buffer is read-only + * @throws IllegalStateException + * if there is no set up header + */ + public final boolean encode(ByteBuffer headerBlock) { + if (!encoding) { + throw new IllegalStateException("A header hasn't been set up"); + } + if (!prependWithCapacityUpdate(headerBlock)) { + return false; + } + boolean done = writer.write(headerTable, headerBlock); + if (done) { + writer.reset(); // FIXME: WHY? + encoding = false; + } + return done; + } + + private boolean prependWithCapacityUpdate(ByteBuffer headerBlock) { + if (capacityUpdate) { + if (!configuredCapacityUpdate) { + List<Integer> sizes = new LinkedList<>(); + if (minCapacity < currCapacity) { + sizes.add((int) minCapacity); + if (minCapacity != lastCapacity) { + sizes.add(lastCapacity); + } + } else if (lastCapacity != currCapacity) { + sizes.add(lastCapacity); + } + bulkSizeUpdateWriter.maxHeaderTableSizes(sizes); + configuredCapacityUpdate = true; + } + boolean done = bulkSizeUpdateWriter.write(headerTable, headerBlock); + if (done) { + minCapacity = lastCapacity; + currCapacity = lastCapacity; + bulkSizeUpdateWriter.reset(); + capacityUpdate = false; + configuredCapacityUpdate = false; + } + return done; + } + return true; + } + + protected final void indexed(int index) throws IndexOutOfBoundsException { + checkEncoding(); + encoding = true; + writer = indexedWriter.index(index); + } + + protected final void literal(int index, CharSequence value, + boolean useHuffman) + throws IndexOutOfBoundsException { + checkEncoding(); + encoding = true; + writer = literalWriter + .index(index).value(value, useHuffman); + } + + protected final void literal(CharSequence name, boolean nameHuffman, + CharSequence value, boolean valueHuffman) { + checkEncoding(); + encoding = true; + writer = literalWriter + .name(name, nameHuffman).value(value, valueHuffman); + } + + protected final void literalNeverIndexed(int index, + CharSequence value, + boolean valueHuffman) + throws IndexOutOfBoundsException { + checkEncoding(); + encoding = true; + writer = literalNeverIndexedWriter + .index(index).value(value, valueHuffman); + } + + protected final void literalNeverIndexed(CharSequence name, + boolean nameHuffman, + CharSequence value, + boolean valueHuffman) { + checkEncoding(); + encoding = true; + writer = literalNeverIndexedWriter + .name(name, nameHuffman).value(value, valueHuffman); + } + + protected final void literalWithIndexing(int index, + CharSequence value, + boolean valueHuffman) + throws IndexOutOfBoundsException { + checkEncoding(); + encoding = true; + writer = literalWithIndexingWriter + .index(index).value(value, valueHuffman); + } + + protected final void literalWithIndexing(CharSequence name, + boolean nameHuffman, + CharSequence value, + boolean valueHuffman) { + checkEncoding(); + encoding = true; + writer = literalWithIndexingWriter + .name(name, nameHuffman).value(value, valueHuffman); + } + + protected final void sizeUpdate(int capacity) + throws IllegalArgumentException { + checkEncoding(); + // Ensure subclass follows the contract + if (capacity > this.maxCapacity) { + throw new IllegalArgumentException( + format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s", + capacity, maxCapacity)); + } + writer = sizeUpdateWriter.maxHeaderTableSize(capacity); + } + + protected final int getMaxCapacity() { + return maxCapacity; + } + + protected final HeaderTable getHeaderTable() { + return headerTable; + } + + protected final void checkEncoding() { + if (encoding) { + throw new IllegalStateException( + "Previous encoding operation hasn't finished yet"); + } + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/HeaderTable.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/HeaderTable.java new file mode 100644 index 00000000000..89820741a32 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/HeaderTable.java @@ -0,0 +1,511 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +import static java.lang.String.format; + +// +// Header Table combined from two tables: static and dynamic. +// +// There is a single address space for index values. Index-aware methods +// correspond to the table as a whole. Size-aware methods only to the dynamic +// part of it. +// +final class HeaderTable { + + private static final HeaderField[] staticTable = { + null, // To make index 1-based, instead of 0-based + new HeaderField(":authority"), + new HeaderField(":method", "GET"), + new HeaderField(":method", "POST"), + new HeaderField(":path", "/"), + new HeaderField(":path", "/index.html"), + new HeaderField(":scheme", "http"), + new HeaderField(":scheme", "https"), + new HeaderField(":status", "200"), + new HeaderField(":status", "204"), + new HeaderField(":status", "206"), + new HeaderField(":status", "304"), + new HeaderField(":status", "400"), + new HeaderField(":status", "404"), + new HeaderField(":status", "500"), + new HeaderField("accept-charset"), + new HeaderField("accept-encoding", "gzip, deflate"), + new HeaderField("accept-language"), + new HeaderField("accept-ranges"), + new HeaderField("accept"), + new HeaderField("access-control-allow-origin"), + new HeaderField("age"), + new HeaderField("allow"), + new HeaderField("authorization"), + new HeaderField("cache-control"), + new HeaderField("content-disposition"), + new HeaderField("content-encoding"), + new HeaderField("content-language"), + new HeaderField("content-length"), + new HeaderField("content-location"), + new HeaderField("content-range"), + new HeaderField("content-type"), + new HeaderField("cookie"), + new HeaderField("date"), + new HeaderField("etag"), + new HeaderField("expect"), + new HeaderField("expires"), + new HeaderField("from"), + new HeaderField("host"), + new HeaderField("if-match"), + new HeaderField("if-modified-since"), + new HeaderField("if-none-match"), + new HeaderField("if-range"), + new HeaderField("if-unmodified-since"), + new HeaderField("last-modified"), + new HeaderField("link"), + new HeaderField("location"), + new HeaderField("max-forwards"), + new HeaderField("proxy-authenticate"), + new HeaderField("proxy-authorization"), + new HeaderField("range"), + new HeaderField("referer"), + new HeaderField("refresh"), + new HeaderField("retry-after"), + new HeaderField("server"), + new HeaderField("set-cookie"), + new HeaderField("strict-transport-security"), + new HeaderField("transfer-encoding"), + new HeaderField("user-agent"), + new HeaderField("vary"), + new HeaderField("via"), + new HeaderField("www-authenticate") + }; + + private static final int STATIC_TABLE_LENGTH = staticTable.length - 1; + private static final int ENTRY_SIZE = 32; + private static final Map<String, LinkedHashMap<String, Integer>> staticIndexes; + + static { + staticIndexes = new HashMap<>(STATIC_TABLE_LENGTH); + for (int i = 1; i <= STATIC_TABLE_LENGTH; i++) { + HeaderField f = staticTable[i]; + Map<String, Integer> values = staticIndexes + .computeIfAbsent(f.name, k -> new LinkedHashMap<>()); + values.put(f.value, i); + } + } + + private final Table dynamicTable = new Table(0); + private int maxSize; + private int size; + + public HeaderTable(int maxSize) { + setMaxSize(maxSize); + } + + // + // The method returns: + // + // * a positive integer i where i (i = [1..Integer.MAX_VALUE]) is an + // index of an entry with a header (n, v), where n.equals(name) && + // v.equals(value) + // + // * a negative integer j where j (j = [-Integer.MAX_VALUE..-1]) is an + // index of an entry with a header (n, v), where n.equals(name) + // + // * 0 if there's no entry e such that e.getName().equals(name) + // + // The rationale behind this design is to allow to pack more useful data + // into a single invocation, facilitating a single pass where possible + // (the idea is the same as in java.util.Arrays.binarySearch(int[], int)). + // + public int indexOf(CharSequence name, CharSequence value) { + // Invoking toString() will possibly allocate Strings for the sake of + // the search, which doesn't feel right. + String n = name.toString(); + String v = value.toString(); + + // 1. Try exact match in the static region + Map<String, Integer> values = staticIndexes.get(n); + if (values != null) { + Integer idx = values.get(v); + if (idx != null) { + return idx; + } + } + // 2. Try exact match in the dynamic region + int didx = dynamicTable.indexOf(n, v); + if (didx > 0) { + return STATIC_TABLE_LENGTH + didx; + } else if (didx < 0) { + if (values != null) { + // 3. Return name match from the static region + return -values.values().iterator().next(); // Iterator allocation + } else { + // 4. Return name match from the dynamic region + return -STATIC_TABLE_LENGTH + didx; + } + } else { + if (values != null) { + // 3. Return name match from the static region + return -values.values().iterator().next(); // Iterator allocation + } else { + return 0; + } + } + } + + public int size() { + return size; + } + + public int maxSize() { + return maxSize; + } + + public int length() { + return STATIC_TABLE_LENGTH + dynamicTable.size(); + } + + HeaderField get(int index) { + checkIndex(index); + if (index <= STATIC_TABLE_LENGTH) { + return staticTable[index]; + } else { + return dynamicTable.get(index - STATIC_TABLE_LENGTH); + } + } + + void put(CharSequence name, CharSequence value) { + // Invoking toString() will possibly allocate Strings. But that's + // unavoidable at this stage. If a CharSequence is going to be stored in + // the table, it must not be mutable (e.g. for the sake of hashing). + put(new HeaderField(name.toString(), value.toString())); + } + + private void put(HeaderField h) { + int entrySize = sizeOf(h); + while (entrySize > maxSize - size && size != 0) { + evictEntry(); + } + if (entrySize > maxSize - size) { + return; + } + size += entrySize; + dynamicTable.add(h); + } + + void setMaxSize(int maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException + ("maxSize >= 0: maxSize=" + maxSize); + } + while (maxSize < size && size != 0) { + evictEntry(); + } + this.maxSize = maxSize; + int upperBound = (maxSize / ENTRY_SIZE) + 1; + this.dynamicTable.setCapacity(upperBound); + } + + HeaderField evictEntry() { + HeaderField f = dynamicTable.remove(); + size -= sizeOf(f); + return f; + } + + @Override + public String toString() { + double used = maxSize == 0 ? 0 : 100 * (((double) size) / maxSize); + return format("entries: %d; used %s/%s (%.1f%%)", dynamicTable.size(), + size, maxSize, used); + } + + int checkIndex(int index) { + if (index < 1 || index > STATIC_TABLE_LENGTH + dynamicTable.size()) { + throw new IllegalArgumentException( + format("1 <= index <= length(): index=%s, length()=%s", + index, length())); + } + return index; + } + + int sizeOf(HeaderField f) { + return f.name.length() + f.value.length() + ENTRY_SIZE; + } + + // + // Diagnostic information in the form used in the RFC 7541 + // + String getStateString() { + if (size == 0) { + return "empty."; + } + + StringBuilder b = new StringBuilder(); + for (int i = 1, size = dynamicTable.size(); i <= size; i++) { + HeaderField e = dynamicTable.get(i); + b.append(format("[%3d] (s = %3d) %s: %s%n", i, + sizeOf(e), e.name, e.value)); + } + b.append(format(" Table size:%4s", this.size)); + return b.toString(); + } + + // Convert to a Value Object (JDK-8046159)? + static final class HeaderField { + + final String name; + final String value; + + public HeaderField(String name) { + this(name, ""); + } + + public HeaderField(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public String toString() { + return value.isEmpty() ? name : name + ": " + value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HeaderField that = (HeaderField) o; + return name.equals(that.name) && value.equals(that.value); + } + + @Override + public int hashCode() { + return 31 * (name.hashCode()) + value.hashCode(); + } + } + + // + // In order to be able to find an index of an entry with the given contents + // in the dynamic table an effective inverse mapping is needed. Here's a + // simple idea behind such a mapping. + // + // # The problem: + // + // We have a queue with an O(1) lookup by index: + // + // get: index -> x + // + // What we also want is an O(1) reverse lookup: + // + // indexOf: x -> index + // + // # Solution: + // + // Let's store an inverse mapping as a Map<X, Integer>. This have a problem + // that when a new element is added to the queue all indexes in the map + // becomes invalid. Namely, each i becomes shifted by 1 to the right: + // + // i -> i + 1 + // + // And the new element is assigned with an index of 1. This would seem to + // require a pass through the map incrementing all indexes (map values) by + // 1, which is O(n). + // + // The good news is we can do much better then this! + // + // Let's create a single field of type long, called 'counter'. Then each + // time a new element 'x' is added to the queue, a value of this field gets + // incremented. Then the resulting value of the 'counter_x' is then put as a + // value under key 'x' to the map: + // + // map.put(x, counter_x) + // + // It gives us a map that maps an element to a value the counter had at the + // time the element had been added. + // + // In order to retrieve an index of any element 'x' in the queue (at any + // given time) we simply need to subtract the value (the snapshot of the + // counter at the time when the 'x' was added) from the current value of the + // counter. This operation basically answers the question: + // + // How many elements ago 'x' was the tail of the queue? + // + // Which is the same as its index in the queue now. Given, of course, it's + // still in the queue. + // + // I'm pretty sure in a real life long overflow will never happen, so it's + // not too practical to add recalibrating code, but a pedantic person might + // want to do so: + // + // if (counter == Long.MAX_VALUE) { + // recalibrate(); + // } + // + // Where 'recalibrate()' goes through the table doing this: + // + // value -= counter + // + // That's given, of course, the size of the table itself is less than + // Long.MAX_VALUE :-) + // + private static final class Table { + + private final Map<String, Map<String, Long>> map; + private final CircularBuffer<HeaderField> buffer; + private long counter = 1; + + Table(int capacity) { + buffer = new CircularBuffer<>(capacity); + map = new HashMap<>(capacity); + } + + void add(HeaderField f) { + buffer.add(f); + Map<String, Long> values = map.computeIfAbsent(f.name, k -> new HashMap<>()); + values.put(f.value, counter++); + } + + HeaderField get(int index) { + return buffer.get(index - 1); + } + + int indexOf(String name, String value) { + Map<String, Long> values = map.get(name); + if (values == null) { + return 0; + } + Long index = values.get(value); + if (index != null) { + return (int) (counter - index); + } else { + assert !values.isEmpty(); + Long any = values.values().iterator().next(); // Iterator allocation + return -(int) (counter - any); + } + } + + HeaderField remove() { + HeaderField f = buffer.remove(); + Map<String, Long> values = map.get(f.name); + Long index = values.remove(f.value); + assert index != null; + if (values.isEmpty()) { + map.remove(f.name); + } + return f; + } + + int size() { + return buffer.size; + } + + public void setCapacity(int capacity) { + buffer.resize(capacity); + } + } + + // head + // v + // [ ][ ][A][B][C][D][ ][ ][ ] + // ^ + // tail + // + // |<- size ->| (4) + // |<------ capacity ------->| (9) + // + static final class CircularBuffer<E> { + + int tail, head, size, capacity; + Object[] elements; + + CircularBuffer(int capacity) { + this.capacity = capacity; + elements = new Object[capacity]; + } + + void add(E elem) { + if (size == capacity) { + throw new IllegalStateException( + format("No room for '%s': capacity=%s", elem, capacity)); + } + elements[head] = elem; + head = (head + 1) % capacity; + size++; + } + + @SuppressWarnings("unchecked") + E remove() { + if (size == 0) { + throw new NoSuchElementException("Empty"); + } + E elem = (E) elements[tail]; + elements[tail] = null; + tail = (tail + 1) % capacity; + size--; + return elem; + } + + @SuppressWarnings("unchecked") + E get(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException( + format("0 <= index <= capacity: index=%s, capacity=%s", + index, capacity)); + } + int idx = (tail + (size - index - 1)) % capacity; + return (E) elements[idx]; + } + + public void resize(int newCapacity) { + if (newCapacity < size) { + throw new IllegalStateException( + format("newCapacity >= size: newCapacity=%s, size=%s", + newCapacity, size)); + } + + Object[] newElements = new Object[newCapacity]; + + if (tail < head || size == 0) { + System.arraycopy(elements, tail, newElements, 0, size); + } else { + System.arraycopy(elements, tail, newElements, 0, elements.length - tail); + System.arraycopy(elements, 0, newElements, elements.length - tail, head); + } + + elements = newElements; + tail = 0; + head = size; + this.capacity = newCapacity; + } + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Huffman.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Huffman.java new file mode 100644 index 00000000000..9c58cc3acc2 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/Huffman.java @@ -0,0 +1,676 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; + +import static java.lang.String.format; + +/** + * Huffman coding table. + * + * <p> Instances of this class are safe for use by multiple threads. + * + * @since 9 + */ +public final class Huffman { + + // TODO: check if reset is done in both reader and writer + + static final class Reader { + + private Node curr; // position in the trie + private int len; // length of the path from the root to 'curr' + private int p; // byte probe + + { + reset(); + } + + public void read(ByteBuffer source, Appendable destination, + boolean isLast) { + read(source, destination, true, isLast); + } + + // Takes 'isLast' rather than returns whether the reading is done or + // not, for more informative exceptions. + void read(ByteBuffer source, Appendable destination, boolean reportEOS, + boolean isLast) { + + Node c = curr; + int l = len; + /* + Since ByteBuffer is itself stateful, its position is + remembered here NOT as a part of Reader's state, + but to set it back in the case of a failure + */ + int pos = source.position(); + + while (source.hasRemaining()) { + int d = source.get(); + for (; p != 0; p >>= 1) { + c = c.getChild(p & d); + l++; + if (c.isLeaf()) { + if (reportEOS && c.isEOSPath) { + throw new IllegalArgumentException("Encountered EOS"); + } + try { + destination.append(c.getChar()); + } catch (RuntimeException | Error e) { + source.position(pos); + throw e; + } catch (IOException e) { + source.position(pos); + throw new UncheckedIOException(e); + } + c = INSTANCE.root; + l = 0; + } + curr = c; + len = l; + } + resetProbe(); + pos++; + } + if (!isLast) { + return; // it's too early to jump to any conclusions, let's wait + } + if (c.isLeaf()) { + return; // it's perfectly ok, no extra padding bits + } + if (c.isEOSPath && len <= 7) { + return; // it's ok, some extra padding bits + } + if (c.isEOSPath) { + throw new IllegalArgumentException( + "Padding is too long (len=" + len + ") " + + "or unexpected end of data"); + } + throw new IllegalArgumentException( + "Not a EOS prefix padding or unexpected end of data"); + } + + public void reset() { + curr = INSTANCE.root; + len = 0; + resetProbe(); + } + + private void resetProbe() { + p = 0x80; + } + } + + static final class Writer { + + private int pos; // position in 'source' + private int avail = 8; // number of least significant bits available in 'curr' + private int curr; // next byte to put to the destination + private int rem; // number of least significant bits in 'code' yet to be processed + private int code; // current code being written + + private CharSequence source; + private int end; + + public Writer from(CharSequence input, int start, int end) { + if (start < 0 || end < 0 || end > input.length() || start > end) { + throw new IndexOutOfBoundsException( + String.format("input.length()=%s, start=%s, end=%s", + input.length(), start, end)); + } + pos = start; + this.end = end; + this.source = input; + return this; + } + + public boolean write(ByteBuffer destination) { + for (; pos < end; pos++) { + if (rem == 0) { + Code desc = INSTANCE.codeOf(source.charAt(pos)); + rem = desc.length; + code = desc.code; + } + while (rem > 0) { + if (rem < avail) { + curr |= (code << (avail - rem)); + avail -= rem; + rem = 0; + } else { + int c = (curr | (code >>> (rem - avail))); + if (destination.hasRemaining()) { + destination.put((byte) c); + } else { + return false; + } + curr = c; + code <<= (32 - rem + avail); // throw written bits off the cliff (is this Sparta?) + code >>>= (32 - rem + avail); // return to the position + rem -= avail; + curr = 0; + avail = 8; + } + } + } + + if (avail < 8) { // have to pad + if (destination.hasRemaining()) { + destination.put((byte) (curr | (INSTANCE.EOS.code >>> (INSTANCE.EOS.length - avail)))); + avail = 8; + } else { + return false; + } + } + + return true; + } + + public Writer reset() { + source = null; + end = -1; + pos = -1; + avail = 8; + curr = 0; + code = 0; + return this; + } + } + + /** + * Shared instance. + */ + public static final Huffman INSTANCE = new Huffman(); + + private final Code EOS = new Code(0x3fffffff, 30); + private final Code[] codes = new Code[257]; + private final Node root = new Node() { + @Override + public String toString() { return "root"; } + }; + + // TODO: consider builder and immutable trie + private Huffman() { + // @formatter:off + addChar(0, 0x1ff8, 13); + addChar(1, 0x7fffd8, 23); + addChar(2, 0xfffffe2, 28); + addChar(3, 0xfffffe3, 28); + addChar(4, 0xfffffe4, 28); + addChar(5, 0xfffffe5, 28); + addChar(6, 0xfffffe6, 28); + addChar(7, 0xfffffe7, 28); + addChar(8, 0xfffffe8, 28); + addChar(9, 0xffffea, 24); + addChar(10, 0x3ffffffc, 30); + addChar(11, 0xfffffe9, 28); + addChar(12, 0xfffffea, 28); + addChar(13, 0x3ffffffd, 30); + addChar(14, 0xfffffeb, 28); + addChar(15, 0xfffffec, 28); + addChar(16, 0xfffffed, 28); + addChar(17, 0xfffffee, 28); + addChar(18, 0xfffffef, 28); + addChar(19, 0xffffff0, 28); + addChar(20, 0xffffff1, 28); + addChar(21, 0xffffff2, 28); + addChar(22, 0x3ffffffe, 30); + addChar(23, 0xffffff3, 28); + addChar(24, 0xffffff4, 28); + addChar(25, 0xffffff5, 28); + addChar(26, 0xffffff6, 28); + addChar(27, 0xffffff7, 28); + addChar(28, 0xffffff8, 28); + addChar(29, 0xffffff9, 28); + addChar(30, 0xffffffa, 28); + addChar(31, 0xffffffb, 28); + addChar(32, 0x14, 6); + addChar(33, 0x3f8, 10); + addChar(34, 0x3f9, 10); + addChar(35, 0xffa, 12); + addChar(36, 0x1ff9, 13); + addChar(37, 0x15, 6); + addChar(38, 0xf8, 8); + addChar(39, 0x7fa, 11); + addChar(40, 0x3fa, 10); + addChar(41, 0x3fb, 10); + addChar(42, 0xf9, 8); + addChar(43, 0x7fb, 11); + addChar(44, 0xfa, 8); + addChar(45, 0x16, 6); + addChar(46, 0x17, 6); + addChar(47, 0x18, 6); + addChar(48, 0x0, 5); + addChar(49, 0x1, 5); + addChar(50, 0x2, 5); + addChar(51, 0x19, 6); + addChar(52, 0x1a, 6); + addChar(53, 0x1b, 6); + addChar(54, 0x1c, 6); + addChar(55, 0x1d, 6); + addChar(56, 0x1e, 6); + addChar(57, 0x1f, 6); + addChar(58, 0x5c, 7); + addChar(59, 0xfb, 8); + addChar(60, 0x7ffc, 15); + addChar(61, 0x20, 6); + addChar(62, 0xffb, 12); + addChar(63, 0x3fc, 10); + addChar(64, 0x1ffa, 13); + addChar(65, 0x21, 6); + addChar(66, 0x5d, 7); + addChar(67, 0x5e, 7); + addChar(68, 0x5f, 7); + addChar(69, 0x60, 7); + addChar(70, 0x61, 7); + addChar(71, 0x62, 7); + addChar(72, 0x63, 7); + addChar(73, 0x64, 7); + addChar(74, 0x65, 7); + addChar(75, 0x66, 7); + addChar(76, 0x67, 7); + addChar(77, 0x68, 7); + addChar(78, 0x69, 7); + addChar(79, 0x6a, 7); + addChar(80, 0x6b, 7); + addChar(81, 0x6c, 7); + addChar(82, 0x6d, 7); + addChar(83, 0x6e, 7); + addChar(84, 0x6f, 7); + addChar(85, 0x70, 7); + addChar(86, 0x71, 7); + addChar(87, 0x72, 7); + addChar(88, 0xfc, 8); + addChar(89, 0x73, 7); + addChar(90, 0xfd, 8); + addChar(91, 0x1ffb, 13); + addChar(92, 0x7fff0, 19); + addChar(93, 0x1ffc, 13); + addChar(94, 0x3ffc, 14); + addChar(95, 0x22, 6); + addChar(96, 0x7ffd, 15); + addChar(97, 0x3, 5); + addChar(98, 0x23, 6); + addChar(99, 0x4, 5); + addChar(100, 0x24, 6); + addChar(101, 0x5, 5); + addChar(102, 0x25, 6); + addChar(103, 0x26, 6); + addChar(104, 0x27, 6); + addChar(105, 0x6, 5); + addChar(106, 0x74, 7); + addChar(107, 0x75, 7); + addChar(108, 0x28, 6); + addChar(109, 0x29, 6); + addChar(110, 0x2a, 6); + addChar(111, 0x7, 5); + addChar(112, 0x2b, 6); + addChar(113, 0x76, 7); + addChar(114, 0x2c, 6); + addChar(115, 0x8, 5); + addChar(116, 0x9, 5); + addChar(117, 0x2d, 6); + addChar(118, 0x77, 7); + addChar(119, 0x78, 7); + addChar(120, 0x79, 7); + addChar(121, 0x7a, 7); + addChar(122, 0x7b, 7); + addChar(123, 0x7ffe, 15); + addChar(124, 0x7fc, 11); + addChar(125, 0x3ffd, 14); + addChar(126, 0x1ffd, 13); + addChar(127, 0xffffffc, 28); + addChar(128, 0xfffe6, 20); + addChar(129, 0x3fffd2, 22); + addChar(130, 0xfffe7, 20); + addChar(131, 0xfffe8, 20); + addChar(132, 0x3fffd3, 22); + addChar(133, 0x3fffd4, 22); + addChar(134, 0x3fffd5, 22); + addChar(135, 0x7fffd9, 23); + addChar(136, 0x3fffd6, 22); + addChar(137, 0x7fffda, 23); + addChar(138, 0x7fffdb, 23); + addChar(139, 0x7fffdc, 23); + addChar(140, 0x7fffdd, 23); + addChar(141, 0x7fffde, 23); + addChar(142, 0xffffeb, 24); + addChar(143, 0x7fffdf, 23); + addChar(144, 0xffffec, 24); + addChar(145, 0xffffed, 24); + addChar(146, 0x3fffd7, 22); + addChar(147, 0x7fffe0, 23); + addChar(148, 0xffffee, 24); + addChar(149, 0x7fffe1, 23); + addChar(150, 0x7fffe2, 23); + addChar(151, 0x7fffe3, 23); + addChar(152, 0x7fffe4, 23); + addChar(153, 0x1fffdc, 21); + addChar(154, 0x3fffd8, 22); + addChar(155, 0x7fffe5, 23); + addChar(156, 0x3fffd9, 22); + addChar(157, 0x7fffe6, 23); + addChar(158, 0x7fffe7, 23); + addChar(159, 0xffffef, 24); + addChar(160, 0x3fffda, 22); + addChar(161, 0x1fffdd, 21); + addChar(162, 0xfffe9, 20); + addChar(163, 0x3fffdb, 22); + addChar(164, 0x3fffdc, 22); + addChar(165, 0x7fffe8, 23); + addChar(166, 0x7fffe9, 23); + addChar(167, 0x1fffde, 21); + addChar(168, 0x7fffea, 23); + addChar(169, 0x3fffdd, 22); + addChar(170, 0x3fffde, 22); + addChar(171, 0xfffff0, 24); + addChar(172, 0x1fffdf, 21); + addChar(173, 0x3fffdf, 22); + addChar(174, 0x7fffeb, 23); + addChar(175, 0x7fffec, 23); + addChar(176, 0x1fffe0, 21); + addChar(177, 0x1fffe1, 21); + addChar(178, 0x3fffe0, 22); + addChar(179, 0x1fffe2, 21); + addChar(180, 0x7fffed, 23); + addChar(181, 0x3fffe1, 22); + addChar(182, 0x7fffee, 23); + addChar(183, 0x7fffef, 23); + addChar(184, 0xfffea, 20); + addChar(185, 0x3fffe2, 22); + addChar(186, 0x3fffe3, 22); + addChar(187, 0x3fffe4, 22); + addChar(188, 0x7ffff0, 23); + addChar(189, 0x3fffe5, 22); + addChar(190, 0x3fffe6, 22); + addChar(191, 0x7ffff1, 23); + addChar(192, 0x3ffffe0, 26); + addChar(193, 0x3ffffe1, 26); + addChar(194, 0xfffeb, 20); + addChar(195, 0x7fff1, 19); + addChar(196, 0x3fffe7, 22); + addChar(197, 0x7ffff2, 23); + addChar(198, 0x3fffe8, 22); + addChar(199, 0x1ffffec, 25); + addChar(200, 0x3ffffe2, 26); + addChar(201, 0x3ffffe3, 26); + addChar(202, 0x3ffffe4, 26); + addChar(203, 0x7ffffde, 27); + addChar(204, 0x7ffffdf, 27); + addChar(205, 0x3ffffe5, 26); + addChar(206, 0xfffff1, 24); + addChar(207, 0x1ffffed, 25); + addChar(208, 0x7fff2, 19); + addChar(209, 0x1fffe3, 21); + addChar(210, 0x3ffffe6, 26); + addChar(211, 0x7ffffe0, 27); + addChar(212, 0x7ffffe1, 27); + addChar(213, 0x3ffffe7, 26); + addChar(214, 0x7ffffe2, 27); + addChar(215, 0xfffff2, 24); + addChar(216, 0x1fffe4, 21); + addChar(217, 0x1fffe5, 21); + addChar(218, 0x3ffffe8, 26); + addChar(219, 0x3ffffe9, 26); + addChar(220, 0xffffffd, 28); + addChar(221, 0x7ffffe3, 27); + addChar(222, 0x7ffffe4, 27); + addChar(223, 0x7ffffe5, 27); + addChar(224, 0xfffec, 20); + addChar(225, 0xfffff3, 24); + addChar(226, 0xfffed, 20); + addChar(227, 0x1fffe6, 21); + addChar(228, 0x3fffe9, 22); + addChar(229, 0x1fffe7, 21); + addChar(230, 0x1fffe8, 21); + addChar(231, 0x7ffff3, 23); + addChar(232, 0x3fffea, 22); + addChar(233, 0x3fffeb, 22); + addChar(234, 0x1ffffee, 25); + addChar(235, 0x1ffffef, 25); + addChar(236, 0xfffff4, 24); + addChar(237, 0xfffff5, 24); + addChar(238, 0x3ffffea, 26); + addChar(239, 0x7ffff4, 23); + addChar(240, 0x3ffffeb, 26); + addChar(241, 0x7ffffe6, 27); + addChar(242, 0x3ffffec, 26); + addChar(243, 0x3ffffed, 26); + addChar(244, 0x7ffffe7, 27); + addChar(245, 0x7ffffe8, 27); + addChar(246, 0x7ffffe9, 27); + addChar(247, 0x7ffffea, 27); + addChar(248, 0x7ffffeb, 27); + addChar(249, 0xffffffe, 28); + addChar(250, 0x7ffffec, 27); + addChar(251, 0x7ffffed, 27); + addChar(252, 0x7ffffee, 27); + addChar(253, 0x7ffffef, 27); + addChar(254, 0x7fffff0, 27); + addChar(255, 0x3ffffee, 26); + addEOS (256, EOS.code, EOS.length); + // @formatter:on + } + + + /** + * Calculates the number of bytes required to represent the given {@code + * CharSequence} with the Huffman coding. + * + * @param value + * characters + * + * @return number of bytes + * + * @throws NullPointerException + * if the value is null + */ + public int lengthOf(CharSequence value) { + return lengthOf(value, 0, value.length()); + } + + /** + * Calculates the number of bytes required to represent a subsequence of the + * given {@code CharSequence} with the Huffman coding. + * + * @param value + * characters + * @param start + * the start index, inclusive + * @param end + * the end index, exclusive + * + * @return number of bytes + * + * @throws NullPointerException + * if the value is null + * @throws IndexOutOfBoundsException + * if any invocation of {@code value.charAt(i)}, where {@code start + * <= i < end} would throw an IndexOutOfBoundsException + */ + public int lengthOf(CharSequence value, int start, int end) { + int len = 0; + for (int i = start; i < end; i++) { + char c = value.charAt(i); + len += INSTANCE.codeOf(c).length; + } + // Integer division with ceiling, assumption: + assert (len / 8 + (len % 8 != 0 ? 1 : 0)) == (len + 7) / 8 : len; + return (len + 7) / 8; + } + + private void addChar(int c, int code, int bitLength) { + addLeaf(c, code, bitLength, false); + codes[c] = new Code(code, bitLength); + } + + private void addEOS(int c, int code, int bitLength) { + addLeaf(c, code, bitLength, true); + codes[c] = new Code(code, bitLength); + } + + private void addLeaf(int c, int code, int bitLength, boolean isEOS) { + if (bitLength < 1) { + throw new IllegalArgumentException("bitLength < 1"); + } + Node curr = root; + for (int p = 1 << bitLength - 1; p != 0 && !curr.isLeaf(); p = p >> 1) { + curr.isEOSPath |= isEOS; // If it's already true, it can't become false + curr = curr.addChildIfAbsent(p & code); + } + curr.isEOSPath |= isEOS; // The last one needs to have this property as well + if (curr.isLeaf()) { + throw new IllegalStateException("Specified code is already taken"); + } + curr.setChar((char) c); + } + + private Code codeOf(char c) { + if (c > 255) { + throw new IllegalArgumentException("char=" + ((int) c)); + } + return codes[c]; + } + + // + // For debugging/testing purposes + // + Node getRoot() { + return root; + } + + // + // Guarantees: + // + // if (isLeaf() == true) => getChar() is a legal call + // if (isLeaf() == false) => getChild(i) is a legal call (though it can + // return null) + // + static class Node { + + Node left; + Node right; + boolean isEOSPath; + + boolean charIsSet; + char c; + + Node getChild(int selector) { + if (isLeaf()) { + throw new IllegalStateException("This is a leaf node"); + } + Node result = selector == 0 ? left : right; + if (result == null) { + throw new IllegalStateException(format( + "Node doesn't have a child (selector=%s)", selector)); + } + return result; + } + + boolean isLeaf() { + return charIsSet; + } + + char getChar() { + if (!isLeaf()) { + throw new IllegalStateException("This node is not a leaf node"); + } + return c; + } + + void setChar(char c) { + if (charIsSet) { + throw new IllegalStateException( + "This node has been taken already"); + } + if (left != null || right != null) { + throw new IllegalStateException("The node cannot be made " + + "a leaf as it's already has a child"); + } + this.c = c; + charIsSet = true; + } + + Node addChildIfAbsent(int i) { + if (charIsSet) { + throw new IllegalStateException("The node cannot have a child " + + "as it's already a leaf node"); + } + Node child; + if (i == 0) { + if ((child = left) == null) { + child = left = new Node(); + } + } else { + if ((child = right) == null) { + child = right = new Node(); + } + } + return child; + } + + @Override + public String toString() { + if (isLeaf()) { + if (isEOSPath) { + return "EOS"; + } else { + return format("char: (%3s) '%s'", (int) c, c); + } + } + return "/\\"; + } + } + + // TODO: value-based class? + // FIXME: can we re-use Node instead of this class? + private static final class Code { + + final int code; + final int length; + + private Code(int code, int length) { + this.code = code; + this.length = length; + } + + public int getCode() { + return code; + } + + public int getLength() { + return length; + } + + @Override + public String toString() { + long p = 1 << length; + return Long.toBinaryString(code + p).substring(1) + + ", length=" + length; + } + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/ISO_8859_1.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/ISO_8859_1.java new file mode 100644 index 00000000000..162c9839ae9 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/ISO_8859_1.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; + +// +// Custom implementation of ISO/IEC 8859-1:1998 +// +// The rationale behind this is not to deal with CharsetEncoder/CharsetDecoder, +// basically because it would require wrapping every single CharSequence into a +// CharBuffer and then copying it back. +// +// But why not to give a CharBuffer instead of Appendable? Because I can choose +// an Appendable (e.g. StringBuilder) that adjusts its length when needed and +// therefore not to deal with pre-sized CharBuffers or copying. +// +// The encoding is simple and well known: 1 byte <-> 1 char +// +final class ISO_8859_1 { + + private ISO_8859_1() { } + + public static final class Reader { + + public void read(ByteBuffer source, Appendable destination) { + for (int i = 0, len = source.remaining(); i < len; i++) { + char c = (char) (source.get() & 0xff); + try { + destination.append(c); + } catch (IOException e) { + throw new UncheckedIOException + ("Error appending to the destination", e); + } + } + } + + public Reader reset() { + return this; + } + } + + public static final class Writer { + + private CharSequence source; + private int pos; + private int end; + + public Writer configure(CharSequence source, int start, int end) { + this.source = source; + this.pos = start; + this.end = end; + return this; + } + + public boolean write(ByteBuffer destination) { + for (; pos < end; pos++) { + char c = source.charAt(pos); + if (c > '\u00FF') { + throw new IllegalArgumentException( + "Illegal ISO-8859-1 char: " + (int) c); + } + if (destination.hasRemaining()) { + destination.put((byte) c); + } else { + return false; + } + } + return true; + } + + public Writer reset() { + source = null; + pos = -1; + end = -1; + return this; + } + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IndexNameValueWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IndexNameValueWriter.java new file mode 100644 index 00000000000..01b4decca00 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IndexNameValueWriter.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; + +abstract class IndexNameValueWriter implements BinaryRepresentationWriter { + + private final int pattern; + private final int prefix; + private final IntegerWriter intWriter = new IntegerWriter(); + private final StringWriter nameWriter = new StringWriter(); + private final StringWriter valueWriter = new StringWriter(); + + protected boolean indexedRepresentation; + + private static final int NEW = 0; + private static final int NAME_PART_WRITTEN = 1; + private static final int VALUE_WRITTEN = 2; + + private int state = NEW; + + protected IndexNameValueWriter(int pattern, int prefix) { + this.pattern = pattern; + this.prefix = prefix; + } + + IndexNameValueWriter index(int index) { + indexedRepresentation = true; + intWriter.configure(index, prefix, pattern); + return this; + } + + IndexNameValueWriter name(CharSequence name, boolean useHuffman) { + indexedRepresentation = false; + intWriter.configure(0, prefix, pattern); + nameWriter.configure(name, useHuffman); + return this; + } + + IndexNameValueWriter value(CharSequence value, boolean useHuffman) { + valueWriter.configure(value, useHuffman); + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + if (state < NAME_PART_WRITTEN) { + if (indexedRepresentation) { + if (!intWriter.write(destination)) { + return false; + } + } else { + if (!intWriter.write(destination) || !nameWriter.write(destination)) { + return false; + } + } + state = NAME_PART_WRITTEN; + } + if (state < VALUE_WRITTEN) { + if (!valueWriter.write(destination)) { + return false; + } + state = VALUE_WRITTEN; + } + return state == VALUE_WRITTEN; + } + + @Override + public IndexNameValueWriter reset() { + intWriter.reset(); + if (!indexedRepresentation) { + nameWriter.reset(); + } + valueWriter.reset(); + state = NEW; + return this; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IndexedWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IndexedWriter.java new file mode 100644 index 00000000000..4ccd9d76112 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IndexedWriter.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; + +final class IndexedWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter = new IntegerWriter(); + + IndexedWriter() { } + + IndexedWriter index(int index) { + intWriter.configure(index, 7, 0b1000_0000); + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + return this; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IntegerReader.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IntegerReader.java new file mode 100644 index 00000000000..0e7abcfd2f5 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IntegerReader.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import static java.lang.String.format; + +final class IntegerReader { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_READ = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int N; + private int maxValue; + private int value; + private long r; + private long b = 1; + + public IntegerReader configure(int N) { + return configure(N, Integer.MAX_VALUE); + } + + // + // Why is it important to configure 'maxValue' here. After all we can wait + // for the integer to be fully read and then check it. Can't we? + // + // Two reasons. + // + // 1. Value wraps around long won't be unnoticed. + // 2. It can spit out an exception as soon as it becomes clear there's + // an overflow. Therefore, no need to wait for the value to be fully read. + // + public IntegerReader configure(int N, int maxValue) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + checkPrefix(N); + if (maxValue < 0) { + throw new IllegalArgumentException( + "maxValue >= 0: maxValue=" + maxValue); + } + this.maxValue = maxValue; + this.N = N; + state = CONFIGURED; + return this; + } + + public boolean read(ByteBuffer input) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + int n = input.get() & max; + if (n != max) { + value = n; + state = DONE; + return true; + } else { + r = max; + } + state = FIRST_BYTE_READ; + } + if (state == FIRST_BYTE_READ) { + // variable-length quantity (VLQ) + byte i; + do { + if (!input.hasRemaining()) { + return false; + } + i = input.get(); + long increment = b * (i & 127); + if (r + increment > maxValue) { + throw new IllegalArgumentException(format( + "Integer overflow: maxValue=%,d, value=%,d", + maxValue, r + increment)); + } + r += increment; + b *= 128; + } while ((128 & i) == 128); + + value = (int) r; + state = DONE; + return true; + } + throw new InternalError(Arrays.toString( + new Object[]{state, N, maxValue, value, r, b})); + } + + public int get() throws IllegalStateException { + if (state != DONE) { + throw new IllegalStateException("Has not been fully read yet"); + } + return value; + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public IntegerReader reset() { + b = 1; + state = NEW; + return this; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IntegerWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IntegerWriter.java new file mode 100644 index 00000000000..7fd1c108ab3 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/IntegerWriter.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +final class IntegerWriter { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int FIRST_BYTE_WRITTEN = 2; + private static final int DONE = 4; + + private int state = NEW; + + private int payload; + private int N; + private int value; + + // + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | | | | | | | | | + // +---+---+---+-------------------+ + // |<--------->|<----------------->| + // payload N=5 + // + // payload is the contents of the left-hand side part of the octet; + // it is truncated to fit into 8-N bits, where 1 <= N <= 8; + // + public IntegerWriter configure(int value, int N, int payload) { + if (state != NEW) { + throw new IllegalStateException("Already configured"); + } + if (value < 0) { + throw new IllegalArgumentException("value >= 0: value=" + value); + } + checkPrefix(N); + this.value = value; + this.N = N; + this.payload = payload & 0xFF & (0xFFFFFFFF << N); + state = CONFIGURED; + return this; + } + + public boolean write(ByteBuffer output) { + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (state == DONE) { + return true; + } + + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + int max = (2 << (N - 1)) - 1; + if (value < max) { + output.put((byte) (payload | value)); + state = DONE; + return true; + } + output.put((byte) (payload | max)); + value -= max; + state = FIRST_BYTE_WRITTEN; + } + if (state == FIRST_BYTE_WRITTEN) { + while (value >= 128 && output.hasRemaining()) { + output.put((byte) (value % 128 + 128)); + value /= 128; + } + if (!output.hasRemaining()) { + return false; + } + output.put((byte) value); + state = DONE; + return true; + } + throw new InternalError(Arrays.toString( + new Object[]{state, payload, N, value})); + } + + private static void checkPrefix(int N) { + if (N < 1 || N > 8) { + throw new IllegalArgumentException("1 <= N <= 8: N= " + N); + } + } + + public IntegerWriter reset() { + state = NEW; + return this; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralNeverIndexedWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralNeverIndexedWriter.java new file mode 100644 index 00000000000..92547ca13a9 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralNeverIndexedWriter.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +final class LiteralNeverIndexedWriter extends IndexNameValueWriter { + + LiteralNeverIndexedWriter() { + super(0b0001_0000, 4); + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralWithIndexingWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralWithIndexingWriter.java new file mode 100644 index 00000000000..5926bd6820e --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralWithIndexingWriter.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; + +final class LiteralWithIndexingWriter extends IndexNameValueWriter { + + private boolean tableUpdated; + + private CharSequence name; + private CharSequence value; + private int index; + + LiteralWithIndexingWriter() { + super(0b0100_0000, 6); + } + + @Override + LiteralWithIndexingWriter index(int index) { + super.index(index); + this.index = index; + return this; + } + + @Override + LiteralWithIndexingWriter name(CharSequence name, boolean useHuffman) { + super.name(name, useHuffman); + this.name = name; + return this; + } + + @Override + LiteralWithIndexingWriter value(CharSequence value, boolean useHuffman) { + super.value(value, useHuffman); + this.value = value; + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + if (!tableUpdated) { + CharSequence n; + if (indexedRepresentation) { + n = table.get(index).name; + } else { + n = name; + } + table.put(n, value); + tableUpdated = true; + } + return super.write(table, destination); + } + + @Override + public IndexNameValueWriter reset() { + tableUpdated = false; + name = null; + value = null; + index = -1; + return super.reset(); + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralWriter.java new file mode 100644 index 00000000000..430dac4a384 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/LiteralWriter.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +final class LiteralWriter extends IndexNameValueWriter { + + LiteralWriter() { + super(0b0000_0000, 4); + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/SizeUpdateWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/SizeUpdateWriter.java new file mode 100644 index 00000000000..5148afeeddc --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/SizeUpdateWriter.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; + +final class SizeUpdateWriter implements BinaryRepresentationWriter { + + private final IntegerWriter intWriter = new IntegerWriter(); + private int maxSize; + private boolean tableUpdated; + + SizeUpdateWriter() { } + + SizeUpdateWriter maxHeaderTableSize(int size) { + intWriter.configure(size, 5, 0b0010_0000); + this.maxSize = size; + return this; + } + + @Override + public boolean write(HeaderTable table, ByteBuffer destination) { + if (!tableUpdated) { + table.setMaxSize(maxSize); + tableUpdated = true; + } + return intWriter.write(destination); + } + + @Override + public BinaryRepresentationWriter reset() { + intWriter.reset(); + maxSize = -1; + tableUpdated = false; + return this; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/StringReader.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/StringReader.java new file mode 100644 index 00000000000..e2bbefb1473 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/StringReader.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +final class StringReader { + + private static final int NEW = 0; + private static final int FIRST_BYTE_READ = 1; + private static final int LENGTH_READ = 2; + private static final int DONE = 4; + + private final IntegerReader intReader = new IntegerReader(); + private final Huffman.Reader huffmanReader = new Huffman.Reader(); + private final ISO_8859_1.Reader plainReader = new ISO_8859_1.Reader(); + + private int state = NEW; + + private boolean huffman; + private int remainingLength; + + boolean read(ByteBuffer input, Appendable output) { + if (state == DONE) { + return true; + } + if (!input.hasRemaining()) { + return false; + } + if (state == NEW) { + int p = input.position(); + huffman = (input.get(p) & 0b10000000) != 0; + state = FIRST_BYTE_READ; + intReader.configure(7); + } + if (state == FIRST_BYTE_READ) { + boolean lengthRead = intReader.read(input); + if (!lengthRead) { + return false; + } + remainingLength = intReader.get(); + state = LENGTH_READ; + } + if (state == LENGTH_READ) { + boolean isLast = input.remaining() >= remainingLength; + int oldLimit = input.limit(); + if (isLast) { + input.limit(input.position() + remainingLength); + } + if (huffman) { + huffmanReader.read(input, output, isLast); + } else { + plainReader.read(input, output); + } + if (isLast) { + input.limit(oldLimit); + } + return isLast; + } + throw new InternalError(Arrays.toString( + new Object[]{state, huffman, remainingLength})); + } + + boolean isHuffmanEncoded() { + if (state < FIRST_BYTE_READ) { + throw new IllegalStateException("Has not been fully read yet"); + } + return huffman; + } + + void reset() { + if (huffman) { + huffmanReader.reset(); + } else { + plainReader.reset(); + } + intReader.reset(); + state = NEW; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/StringWriter.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/StringWriter.java new file mode 100644 index 00000000000..5c58e371082 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/StringWriter.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | H | String Length (7+) | +// +---+---------------------------+ +// | String Data (Length octets) | +// +-------------------------------+ +// +// StringWriter does not require a notion of endOfInput (isLast) in 'write' +// methods due to the nature of string representation in HPACK. Namely, the +// length of the string is put before string's contents. Therefore the length is +// always known beforehand. +// +// Expected use: +// +// configure write* (reset configure write*)* +// +final class StringWriter { + + private static final int NEW = 0; + private static final int CONFIGURED = 1; + private static final int LENGTH_WRITTEN = 2; + private static final int DONE = 4; + + private final IntegerWriter intWriter = new IntegerWriter(); + private final Huffman.Writer huffmanWriter = new Huffman.Writer(); + private final ISO_8859_1.Writer plainWriter = new ISO_8859_1.Writer(); + + private int state = NEW; + private boolean huffman; + + StringWriter configure(CharSequence input, boolean huffman) { + return configure(input, 0, input.length(), huffman); + } + + StringWriter configure(CharSequence input, int start, int end, + boolean huffman) { + if (start < 0 || end < 0 || end > input.length() || start > end) { + throw new IndexOutOfBoundsException( + String.format("input.length()=%s, start=%s, end=%s", + input.length(), start, end)); + } + if (!huffman) { + plainWriter.configure(input, start, end); + intWriter.configure(end - start, 7, 0b0000_0000); + } else { + huffmanWriter.from(input, start, end); + intWriter.configure(Huffman.INSTANCE.lengthOf(input, start, end), + 7, 0b1000_0000); + } + + this.huffman = huffman; + state = CONFIGURED; + return this; + } + + boolean write(ByteBuffer output) { + if (state == DONE) { + return true; + } + if (state == NEW) { + throw new IllegalStateException("Configure first"); + } + if (!output.hasRemaining()) { + return false; + } + if (state == CONFIGURED) { + if (intWriter.write(output)) { + state = LENGTH_WRITTEN; + } else { + return false; + } + } + if (state == LENGTH_WRITTEN) { + boolean written = huffman + ? huffmanWriter.write(output) + : plainWriter.write(output); + if (written) { + state = DONE; + return true; + } else { + return false; + } + } + throw new InternalError(Arrays.toString(new Object[]{state, huffman})); + } + + void reset() { + intWriter.reset(); + if (huffman) { + huffmanWriter.reset(); + } else { + plainWriter.reset(); + } + state = NEW; + } +} diff --git a/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/package-info.java b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/package-info.java new file mode 100644 index 00000000000..5c035ff4662 --- /dev/null +++ b/jdk/src/java.httpclient/share/classes/sun/net/httpclient/hpack/package-info.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +/** + * HPACK (Header Compression for HTTP/2) implementation conforming to + * <a href="https://tools.ietf.org/html/rfc7541">RFC 7541</a>. + * + * <p> Headers can be decoded and encoded by {@link sun.net.httpclient.hpack.Decoder} + * and {@link sun.net.httpclient.hpack.Encoder} respectively. + * + * <p> Instances of these classes are not safe for use by multiple threads. + */ +package sun.net.httpclient.hpack; diff --git a/jdk/test/java/net/httpclient/http2/HpackDriver.java b/jdk/test/java/net/httpclient/http2/HpackDriver.java new file mode 100644 index 00000000000..3bf1bc39a4b --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/HpackDriver.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8153353 + * @modules java.httpclient/sun.net.httpclient.hpack + * @key randomness + * @compile/module=java.httpclient sun/net/httpclient/hpack/SpecHelper.java + * @compile/module=java.httpclient sun/net/httpclient/hpack/TestHelper.java + * @compile/module=java.httpclient sun/net/httpclient/hpack/BuffersTestingKit.java + * @run testng/othervm -XaddReads:java.httpclient=ALL-UNNAMED java.httpclient/sun.net.httpclient.hpack.BinaryPrimitivesTest + * @run testng/othervm -XaddReads:java.httpclient=ALL-UNNAMED java.httpclient/sun.net.httpclient.hpack.CircularBufferTest + * @run testng/othervm -XaddReads:java.httpclient=ALL-UNNAMED java.httpclient/sun.net.httpclient.hpack.DecoderTest + * @run testng/othervm -XaddReads:java.httpclient=ALL-UNNAMED java.httpclient/sun.net.httpclient.hpack.EncoderTest + * @run testng/othervm -XaddReads:java.httpclient=ALL-UNNAMED java.httpclient/sun.net.httpclient.hpack.HeaderTableTest + * @run testng/othervm -XaddReads:java.httpclient=ALL-UNNAMED java.httpclient/sun.net.httpclient.hpack.HuffmanTest + * @run testng/othervm -XaddReads:java.httpclient=ALL-UNNAMED java.httpclient/sun.net.httpclient.hpack.TestHelper + */ +public class HpackDriver { } diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/BinaryPrimitivesTest.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/BinaryPrimitivesTest.java new file mode 100644 index 00000000000..dedd53329a8 --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/BinaryPrimitivesTest.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; +import static sun.net.httpclient.hpack.BuffersTestingKit.*; +import static sun.net.httpclient.hpack.TestHelper.newRandom; + +// +// Some of the tests below overlap in what they test. This allows to diagnose +// bugs quicker and with less pain by simply ruling out common working bits. +// +public final class BinaryPrimitivesTest { + + private final Random rnd = newRandom(); + + @Test + public void integerRead1() { + verifyRead(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5); + } + + @Test + public void integerRead2() { + verifyRead(bytes(0b00001010), 10, 5); + } + + @Test + public void integerRead3() { + verifyRead(bytes(0b00101010), 42, 8); + } + + @Test + public void integerWrite1() { + verifyWrite(bytes(0b00011111, 0b10011010, 0b00001010), 1337, 5); + } + + @Test + public void integerWrite2() { + verifyWrite(bytes(0b00001010), 10, 5); + } + + @Test + public void integerWrite3() { + verifyWrite(bytes(0b00101010), 42, 8); + } + + // + // Since readInteger(x) is the inverse of writeInteger(x), thus: + // + // for all x: readInteger(writeInteger(x)) == x + // + @Test + public void integerIdentity() { + final int MAX_VALUE = 1 << 22; + int totalCases = 0; + int maxFilling = 0; + IntegerReader r = new IntegerReader(); + IntegerWriter w = new IntegerWriter(); + ByteBuffer buf = ByteBuffer.allocate(8); + for (int N = 1; N < 9; N++) { + for (int expected = 0; expected <= MAX_VALUE; expected++) { + w.reset().configure(expected, N, 1).write(buf); + buf.flip(); + totalCases++; + maxFilling = Math.max(maxFilling, buf.remaining()); + r.reset().configure(N).read(buf); + assertEquals(r.get(), expected); + buf.clear(); + } + } + System.out.printf("totalCases: %,d, maxFilling: %,d, maxValue: %,d%n", + totalCases, maxFilling, MAX_VALUE); + } + + @Test + public void integerReadChunked() { + final int NUM_TESTS = 1024; + IntegerReader r = new IntegerReader(); + ByteBuffer bb = ByteBuffer.allocate(8); + IntegerWriter w = new IntegerWriter(); + for (int i = 0; i < NUM_TESTS; i++) { + final int N = 1 + rnd.nextInt(8); + final int expected = rnd.nextInt(Integer.MAX_VALUE) + 1; + w.reset().configure(expected, N, rnd.nextInt()).write(bb); + bb.flip(); + + forEachSplit(bb, + (buffers) -> { + Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers)); + r.configure(N); + for (ByteBuffer b : buf) { + r.read(b); + } + assertEquals(r.get(), expected); + r.reset(); + }); + bb.clear(); + } + } + + // FIXME: use maxValue in the test + + @Test + // FIXME: tune values for better coverage + public void integerWriteChunked() { + ByteBuffer bb = ByteBuffer.allocate(6); + IntegerWriter w = new IntegerWriter(); + IntegerReader r = new IntegerReader(); + for (int i = 0; i < 1024; i++) { // number of tests + final int N = 1 + rnd.nextInt(8); + final int payload = rnd.nextInt(255); + final int expected = rnd.nextInt(Integer.MAX_VALUE) + 1; + + forEachSplit(bb, + (buffers) -> { + List<ByteBuffer> buf = new ArrayList<>(); + relocateBuffers(injectEmptyBuffers(buffers)).forEach(buf::add); + boolean written = false; + w.configure(expected, N, payload); // TODO: test for payload it can be read after written + for (ByteBuffer b : buf) { + int pos = b.position(); + written = w.write(b); + b.position(pos); + } + if (!written) { + fail("please increase bb size"); + } + r.configure(N).read(concat(buf)); + // TODO: check payload here + assertEquals(r.get(), expected); + w.reset(); + r.reset(); + bb.clear(); + }); + } + } + + + // + // Since readString(x) is the inverse of writeString(x), thus: + // + // for all x: readString(writeString(x)) == x + // + @Test + public void stringIdentity() { + final int MAX_STRING_LENGTH = 4096; + ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); // it takes 6 bytes to encode string length of Integer.MAX_VALUE + CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); + StringReader reader = new StringReader(); + StringWriter writer = new StringWriter(); + for (int len = 0; len <= MAX_STRING_LENGTH; len++) { + for (int i = 0; i < 64; i++) { + // not so much "test in isolation", I know... we're testing .reset() as well + bytes.clear(); + chars.clear(); + + byte[] b = new byte[len]; + rnd.nextBytes(b); + + String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string + + boolean written = writer + .configure(CharBuffer.wrap(expected), 0, expected.length(), false) + .write(bytes); + + if (!written) { + fail("please increase 'bytes' size"); + } + bytes.flip(); + reader.read(bytes, chars); + chars.flip(); + assertEquals(chars.toString(), expected); + reader.reset(); + writer.reset(); + } + } + } + +// @Test +// public void huffmanStringWriteChunked() { +// fail(); +// } +// +// @Test +// public void huffmanStringReadChunked() { +// fail(); +// } + + @Test + public void stringWriteChunked() { + final int MAX_STRING_LENGTH = 8; + final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); + final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); + final StringReader reader = new StringReader(); + final StringWriter writer = new StringWriter(); + for (int len = 0; len <= MAX_STRING_LENGTH; len++) { + + byte[] b = new byte[len]; + rnd.nextBytes(b); + + String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string + + forEachSplit(bytes, (buffers) -> { + writer.configure(expected, 0, expected.length(), false); + boolean written = false; + for (ByteBuffer buf : buffers) { + int p0 = buf.position(); + written = writer.write(buf); + buf.position(p0); + } + if (!written) { + fail("please increase 'bytes' size"); + } + reader.read(concat(buffers), chars); + chars.flip(); + assertEquals(chars.toString(), expected); + reader.reset(); + writer.reset(); + chars.clear(); + bytes.clear(); + }); + } + } + + @Test + public void stringReadChunked() { + final int MAX_STRING_LENGTH = 16; + final ByteBuffer bytes = ByteBuffer.allocate(MAX_STRING_LENGTH + 6); + final CharBuffer chars = CharBuffer.allocate(MAX_STRING_LENGTH); + final StringReader reader = new StringReader(); + final StringWriter writer = new StringWriter(); + for (int len = 0; len <= MAX_STRING_LENGTH; len++) { + + byte[] b = new byte[len]; + rnd.nextBytes(b); + + String expected = new String(b, StandardCharsets.ISO_8859_1); // reference string + + boolean written = writer + .configure(CharBuffer.wrap(expected), 0, expected.length(), false) + .write(bytes); + writer.reset(); + + if (!written) { + fail("please increase 'bytes' size"); + } + bytes.flip(); + + forEachSplit(bytes, (buffers) -> { + for (ByteBuffer buf : buffers) { + int p0 = buf.position(); + reader.read(buf, chars); + buf.position(p0); + } + chars.flip(); + assertEquals(chars.toString(), expected); + reader.reset(); + chars.clear(); + }); + + bytes.clear(); + } + } + +// @Test +// public void test_Huffman_String_Identity() { +// StringWriter writer = new StringWriter(); +// StringReader reader = new StringReader(); +// // 256 * 8 gives 2048 bits in case of plain 8 bit coding +// // 256 * 30 gives you 7680 bits or 960 bytes in case of almost +// // improbable event of 256 30 bits symbols in a row +// ByteBuffer binary = ByteBuffer.allocate(960); +// CharBuffer text = CharBuffer.allocate(960 / 5); // 5 = minimum code length +// for (int len = 0; len < 128; len++) { +// for (int i = 0; i < 256; i++) { +// // not so much "test in isolation", I know... +// binary.clear(); +// +// byte[] bytes = new byte[len]; +// rnd.nextBytes(bytes); +// +// String s = new String(bytes, StandardCharsets.ISO_8859_1); +// +// writer.write(CharBuffer.wrap(s), binary, true); +// binary.flip(); +// reader.read(binary, text); +// text.flip(); +// assertEquals(text.toString(), s); +// } +// } +// } + + // TODO: atomic failures: e.g. readonly/overflow + + private static byte[] bytes(int... data) { + byte[] bytes = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + bytes[i] = (byte) data[i]; + } + return bytes; + } + + private static void verifyRead(byte[] data, int expected, int N) { + ByteBuffer buf = ByteBuffer.wrap(data, 0, data.length); + IntegerReader reader = new IntegerReader(); + reader.configure(N).read(buf); + assertEquals(expected, reader.get()); + } + + private void verifyWrite(byte[] expected, int data, int N) { + IntegerWriter w = new IntegerWriter(); + ByteBuffer buf = ByteBuffer.allocate(2 * expected.length); + w.configure(data, N, 1).write(buf); + buf.flip(); + assertEquals(ByteBuffer.wrap(expected), buf); + } +} diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/BuffersTestingKit.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/BuffersTestingKit.java new file mode 100644 index 00000000000..4f9631647c7 --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/BuffersTestingKit.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static java.nio.ByteBuffer.allocate; + +public final class BuffersTestingKit { + + /** + * Relocates a {@code [position, limit)} region of the given buffer to + * corresponding region in a new buffer starting with provided {@code + * newPosition}. + * + * <p> Might be useful to make sure ByteBuffer's users do not rely on any + * absolute positions, but solely on what's reported by position(), limit(). + * + * <p> The contents between the given buffer and the returned one are not + * shared. + */ + public static ByteBuffer relocate(ByteBuffer buffer, int newPosition, + int newCapacity) { + int oldPosition = buffer.position(); + int oldLimit = buffer.limit(); + + if (newPosition + oldLimit - oldPosition > newCapacity) { + throw new IllegalArgumentException(); + } + + ByteBuffer result; + if (buffer.isDirect()) { + result = ByteBuffer.allocateDirect(newCapacity); + } else { + result = allocate(newCapacity); + } + + result.position(newPosition); + result.put(buffer).limit(result.position()).position(newPosition); + buffer.position(oldPosition); + + if (buffer.isReadOnly()) { + return result.asReadOnlyBuffer(); + } + return result; + } + + public static Iterable<? extends ByteBuffer> relocateBuffers( + Iterable<? extends ByteBuffer> source) { + return () -> + new Iterator<ByteBuffer>() { + + private final Iterator<? extends ByteBuffer> it = source.iterator(); + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + @Override + public ByteBuffer next() { + ByteBuffer buf = it.next(); + int remaining = buf.remaining(); + int newCapacity = remaining + random.nextInt(17); + int newPosition = random.nextInt(newCapacity - remaining + 1); + return relocate(buf, newPosition, newCapacity); + } + }; + } + + // TODO: not always of size 0 (it's fine for buffer to report !b.hasRemaining()) + public static Iterable<? extends ByteBuffer> injectEmptyBuffers( + Iterable<? extends ByteBuffer> source) { + return injectEmptyBuffers(source, () -> allocate(0)); + } + + public static Iterable<? extends ByteBuffer> injectEmptyBuffers( + Iterable<? extends ByteBuffer> source, + Supplier<? extends ByteBuffer> emptyBufferFactory) { + + return () -> + new Iterator<ByteBuffer>() { + + private final Iterator<? extends ByteBuffer> it = source.iterator(); + private ByteBuffer next = calculateNext(); + + private ByteBuffer calculateNext() { + if (random.nextBoolean()) { + return emptyBufferFactory.get(); + } else if (it.hasNext()) { + return it.next(); + } else { + return null; + } + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public ByteBuffer next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + ByteBuffer next = this.next; + this.next = calculateNext(); + return next; + } + }; + } + + public static ByteBuffer concat(Iterable<? extends ByteBuffer> split) { + return concat(split, ByteBuffer::allocate); + } + + public static ByteBuffer concat(Iterable<? extends ByteBuffer> split, + Function<? super Integer, ? extends ByteBuffer> concatBufferFactory) { + int size = 0; + for (ByteBuffer bb : split) { + size += bb.remaining(); + } + + ByteBuffer result = concatBufferFactory.apply(size); + for (ByteBuffer bb : split) { + result.put(bb); + } + + result.flip(); + return result; + } + + public static void forEachSplit(ByteBuffer bb, + Consumer<? super Iterable<? extends ByteBuffer>> action) { + forEachSplit(bb.remaining(), + (lengths) -> { + int end = bb.position(); + List<ByteBuffer> buffers = new LinkedList<>(); + for (int len : lengths) { + ByteBuffer d = bb.duplicate(); + d.position(end); + d.limit(end + len); + end += len; + buffers.add(d); + } + action.accept(buffers); + }); + } + + private static void forEachSplit(int n, Consumer<? super Iterable<? extends Integer>> action) { + forEachSplit(n, new Stack<>(), action); + } + + private static void forEachSplit(int n, Stack<Integer> path, + Consumer<? super Iterable<? extends Integer>> action) { + if (n == 0) { + action.accept(path); + } else { + for (int i = 1; i <= n; i++) { + path.push(i); + forEachSplit(n - i, path, action); + path.pop(); + } + } + } + + private static final Random random = new Random(); + + private BuffersTestingKit() { + throw new InternalError(); + } + +// public static void main(String[] args) { +// +// List<ByteBuffer> buffers = Arrays.asList( +// (ByteBuffer) allocate(3).position(1).limit(2), +// allocate(0), +// allocate(7)); +// +// Iterable<? extends ByteBuffer> buf = relocateBuffers(injectEmptyBuffers(buffers)); +// List<ByteBuffer> result = new ArrayList<>(); +// buf.forEach(result::add); +// System.out.println(result); +// } +} diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/CircularBufferTest.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/CircularBufferTest.java new file mode 100644 index 00000000000..ebf1cb1d1ee --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/CircularBufferTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import sun.net.httpclient.hpack.HeaderTable.CircularBuffer; + +import java.util.Queue; +import java.util.Random; +import java.util.concurrent.ArrayBlockingQueue; + +import static org.testng.Assert.assertEquals; +import static sun.net.httpclient.hpack.TestHelper.newRandom; + +public final class CircularBufferTest { + + private final Random r = newRandom(); + + @BeforeClass + public void setUp() { + r.setSeed(System.currentTimeMillis()); + } + + @Test + public void queue() { + for (int capacity = 1; capacity <= 2048; capacity++) { + queueOnce(capacity, 32); + } + } + + @Test + public void resize() { + for (int capacity = 1; capacity <= 4096; capacity++) { + resizeOnce(capacity); + } + } + + @Test + public void downSizeEmptyBuffer() { + CircularBuffer<Integer> buffer = new CircularBuffer<>(16); + buffer.resize(15); + } + + private void resizeOnce(int capacity) { + + int nextNumberToPut = 0; + + Queue<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity); + CircularBuffer<Integer> buffer = new CircularBuffer<>(capacity); + + // Fill full, so the next add will wrap + for (int i = 0; i < capacity; i++, nextNumberToPut++) { + buffer.add(nextNumberToPut); + referenceQueue.add(nextNumberToPut); + } + int gets = r.nextInt(capacity); // [0, capacity) + for (int i = 0; i < gets; i++) { + referenceQueue.poll(); + buffer.remove(); + } + int puts = r.nextInt(gets + 1); // [0, gets] + for (int i = 0; i < puts; i++, nextNumberToPut++) { + buffer.add(nextNumberToPut); + referenceQueue.add(nextNumberToPut); + } + + Integer[] expected = referenceQueue.toArray(new Integer[0]); + buffer.resize(expected.length); + + assertEquals(buffer.elements, expected); + } + + private void queueOnce(int capacity, int numWraps) { + + Queue<Integer> referenceQueue = new ArrayBlockingQueue<>(capacity); + CircularBuffer<Integer> buffer = new CircularBuffer<>(capacity); + + int nextNumberToPut = 0; + int totalPuts = 0; + int putsLimit = capacity * numWraps; + int remainingCapacity = capacity; + int size = 0; + + while (totalPuts < putsLimit) { + assert remainingCapacity + size == capacity; + int puts = r.nextInt(remainingCapacity + 1); // [0, remainingCapacity] + remainingCapacity -= puts; + size += puts; + for (int i = 0; i < puts; i++, nextNumberToPut++) { + referenceQueue.add(nextNumberToPut); + buffer.add(nextNumberToPut); + } + totalPuts += puts; + int gets = r.nextInt(size + 1); // [0, size] + size -= gets; + remainingCapacity += gets; + for (int i = 0; i < gets; i++) { + Integer expected = referenceQueue.poll(); + Integer actual = buffer.remove(); + assertEquals(actual, expected); + } + } + } +} diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/DecoderTest.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/DecoderTest.java new file mode 100644 index 00000000000..29a651fb0f7 --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/DecoderTest.java @@ -0,0 +1,595 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import org.testng.annotations.Test; + +import java.io.UncheckedIOException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static sun.net.httpclient.hpack.TestHelper.*; + +public final class DecoderTest { + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.1 + // + @Test + public void example1() { + // @formatter:off + test("400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" + + "746f 6d2d 6865 6164 6572", + + "[ 1] (s = 55) custom-key: custom-header\n" + + " Table size: 55", + + "custom-key: custom-header"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.2 + // + @Test + public void example2() { + // @formatter:off + test("040c 2f73 616d 706c 652f 7061 7468", + "empty.", + ":path: /sample/path"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.3 + // + @Test + public void example3() { + // @formatter:off + test("1008 7061 7373 776f 7264 0673 6563 7265\n" + + "74", + "empty.", + "password: secret"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.4 + // + @Test + public void example4() { + // @formatter:off + test("82", + "empty.", + ":method: GET"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.3 + // + @Test + public void example5() { + // @formatter:off + Decoder d = new Decoder(256); + + test(d, "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + + "2e63 6f6d", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com"); + + test(d, "8286 84be 5808 6e6f 2d63 6163 6865", + + "[ 1] (s = 53) cache-control: no-cache\n" + + "[ 2] (s = 57) :authority: www.example.com\n" + + " Table size: 110", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com\n" + + "cache-control: no-cache"); + + test(d, "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" + + "0c63 7573 746f 6d2d 7661 6c75 65", + + "[ 1] (s = 54) custom-key: custom-value\n" + + "[ 2] (s = 53) cache-control: no-cache\n" + + "[ 3] (s = 57) :authority: www.example.com\n" + + " Table size: 164", + + ":method: GET\n" + + ":scheme: https\n" + + ":path: /index.html\n" + + ":authority: www.example.com\n" + + "custom-key: custom-value"); + + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.4 + // + @Test + public void example6() { + // @formatter:off + Decoder d = new Decoder(256); + + test(d, "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" + + "ff", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com"); + + test(d, "8286 84be 5886 a8eb 1064 9cbf", + + "[ 1] (s = 53) cache-control: no-cache\n" + + "[ 2] (s = 57) :authority: www.example.com\n" + + " Table size: 110", + + ":method: GET\n" + + ":scheme: http\n" + + ":path: /\n" + + ":authority: www.example.com\n" + + "cache-control: no-cache"); + + test(d, "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" + + "a849 e95b b8e8 b4bf", + + "[ 1] (s = 54) custom-key: custom-value\n" + + "[ 2] (s = 53) cache-control: no-cache\n" + + "[ 3] (s = 57) :authority: www.example.com\n" + + " Table size: 164", + + ":method: GET\n" + + ":scheme: https\n" + + ":path: /index.html\n" + + ":authority: www.example.com\n" + + "custom-key: custom-value"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.5 + // + @Test + public void example7() { + // @formatter:off + Decoder d = new Decoder(256); + + test(d, "4803 3330 3258 0770 7269 7661 7465 611d\n" + + "4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" + + "2032 303a 3133 3a32 3120 474d 546e 1768\n" + + "7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" + + "6c65 2e63 6f6d", + + "[ 1] (s = 63) location: https://www.example.com\n" + + "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 3] (s = 52) cache-control: private\n" + + "[ 4] (s = 42) :status: 302\n" + + " Table size: 222", + + ":status: 302\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "location: https://www.example.com"); + + test(d, "4803 3330 37c1 c0bf", + + "[ 1] (s = 42) :status: 307\n" + + "[ 2] (s = 63) location: https://www.example.com\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 4] (s = 52) cache-control: private\n" + + " Table size: 222", + + ":status: 307\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "location: https://www.example.com"); + + test(d, "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" + + "3230 3133 2032 303a 3133 3a32 3220 474d\n" + + "54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" + + "444a 4b48 514b 425a 584f 5157 454f 5049\n" + + "5541 5851 5745 4f49 553b 206d 6178 2d61\n" + + "6765 3d33 3630 303b 2076 6572 7369 6f6e\n" + + "3d31", + + "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + + "[ 2] (s = 52) content-encoding: gzip\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + " Table size: 215", + + ":status: 200\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + "location: https://www.example.com\n" + + "content-encoding: gzip\n" + + "set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.6 + // + @Test + public void example8() { + // @formatter:off + Decoder d = new Decoder(256); + + test(d, "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" + + "9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" + + "2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" + + "e9ae 82ae 43d3", + + "[ 1] (s = 63) location: https://www.example.com\n" + + "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 3] (s = 52) cache-control: private\n" + + "[ 4] (s = 42) :status: 302\n" + + " Table size: 222", + + ":status: 302\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "location: https://www.example.com"); + + test(d, "4883 640e ffc1 c0bf", + + "[ 1] (s = 42) :status: 307\n" + + "[ 2] (s = 63) location: https://www.example.com\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 4] (s = 52) cache-control: private\n" + + " Table size: 222", + + ":status: 307\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "location: https://www.example.com"); + + test(d, "88c1 6196 d07a be94 1054 d444 a820 0595\n" + + "040b 8166 e084 a62d 1bff c05a 839b d9ab\n" + + "77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" + + "3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" + + "9587 3160 65c0 03ed 4ee5 b106 3d50 07", + + "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + + "[ 2] (s = 52) content-encoding: gzip\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + " Table size: 215", + + ":status: 200\n" + + "cache-control: private\n" + + "date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + "location: https://www.example.com\n" + + "content-encoding: gzip\n" + + "set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); + // @formatter:on + } + + @Test + // One of responses from Apache Server that helped to catch a bug + public void testX() { + Decoder d = new Decoder(4096); + // @formatter:off + test(d, "3fe1 1f88 6196 d07a be94 03ea 693f 7504\n" + + "00b6 a05c b827 2e32 fa98 b46f 769e 86b1\n" + + "9272 b025 da5c 2ea9 fd70 a8de 7fb5 3556\n" + + "5ab7 6ece c057 02e2 2ad2 17bf 6c96 d07a\n" + + "be94 0854 cb6d 4a08 0075 40bd 71b6 6e05\n" + + "a531 68df 0f13 8efe 4522 cd32 21b6 5686\n" + + "eb23 781f cf52 848f d24a 8f0f 0d02 3435\n" + + "5f87 497c a589 d34d 1f", + + "[ 1] (s = 53) content-type: text/html\n" + + "[ 2] (s = 50) accept-ranges: bytes\n" + + "[ 3] (s = 74) last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" + + "[ 4] (s = 77) server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" + + "[ 5] (s = 65) date: Mon, 09 Nov 2015 16:26:39 GMT\n" + + " Table size: 319", + + ":status: 200\n" + + "date: Mon, 09 Nov 2015 16:26:39 GMT\n" + + "server: Apache/2.4.17 (Unix) OpenSSL/1.0.2e-dev\n" + + "last-modified: Mon, 11 Jun 2007 18:53:14 GMT\n" + + "etag: \"2d-432a5e4a73a80\"\n" + + "accept-ranges: bytes\n" + + "content-length: 45\n" + + "content-type: text/html"); + // @formatter:on + } + + // + // This test is missing in the spec + // + @Test + public void sizeUpdate() { + Decoder d = new Decoder(4096); + assertEquals(d.getTable().maxSize(), 4096); + d.decode(ByteBuffer.wrap(new byte[]{0b00111110}), true, nopCallback()); // newSize = 30 + assertEquals(d.getTable().maxSize(), 30); + } + + @Test + public void incorrectSizeUpdate() { + ByteBuffer b = ByteBuffer.allocate(8); + Encoder e = new Encoder(8192) { + @Override + protected int calculateCapacity(int maxCapacity) { + return maxCapacity; + } + }; + e.header("a", "b"); + e.encode(b); + b.flip(); + { + Decoder d = new Decoder(4096); + UncheckedIOException ex = assertVoidThrows(UncheckedIOException.class, + () -> d.decode(b, true, (name, value) -> { })); + + assertNotNull(ex.getCause()); + assertEquals(ex.getCause().getClass(), ProtocolException.class); + } + b.flip(); + { + Decoder d = new Decoder(4096); + UncheckedIOException ex = assertVoidThrows(UncheckedIOException.class, + () -> d.decode(b, false, (name, value) -> { })); + + assertNotNull(ex.getCause()); + assertEquals(ex.getCause().getClass(), ProtocolException.class); + } + } + + @Test + public void corruptedHeaderBlockInteger() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + (byte) 0b11111111, // indexed + (byte) 0b10011010 // 25 + ... + }); + UncheckedIOException e = assertVoidThrows(UncheckedIOException.class, + () -> d.decode(data, true, nopCallback())); + assertNotNull(e.getCause()); + assertEquals(e.getCause().getClass(), ProtocolException.class); + assertExceptionMessageContains(e, "Unexpected end of header block"); + } + + // 5.1. Integer Representation + // ... + // Integer encodings that exceed implementation limits -- in value or octet + // length -- MUST be treated as decoding errors. Different limits can + // be set for each of the different uses of integers, based on + // implementation constraints. + @Test + public void headerBlockIntegerNoOverflow() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + (byte) 0b11111111, // indexed + 127 + // Integer.MAX_VALUE - 127 (base 128, little-endian): + (byte) 0b10000000, + (byte) 0b11111111, + (byte) 0b11111111, + (byte) 0b11111111, + (byte) 0b00000111 + }); + + IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "index=2147483647"); + } + + @Test + public void headerBlockIntegerOverflow() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + (byte) 0b11111111, // indexed + 127 + // Integer.MAX_VALUE - 127 + 1 (base 128, little endian): + (byte) 0b10000001, + (byte) 0b11111111, + (byte) 0b11111111, + (byte) 0b11111111, + (byte) 0b00000111 + }); + + IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "Integer overflow"); + } + + @Test + public void corruptedHeaderBlockString1() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + 0b00001000, // huffman=false, length=8 + 0b00000000, // \ + 0b00000000, // but only 3 octets available... + 0b00000000 // / + }); + UncheckedIOException e = assertVoidThrows(UncheckedIOException.class, + () -> d.decode(data, true, nopCallback())); + assertNotNull(e.getCause()); + assertEquals(e.getCause().getClass(), ProtocolException.class); + assertExceptionMessageContains(e, "Unexpected end of header block"); + } + + @Test + public void corruptedHeaderBlockString2() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10001000, // huffman=true, length=8 + 0b00000000, // \ + 0b00000000, // \ + 0b00000000, // but only 5 octets available... + 0b00000000, // / + 0b00000000 // / + }); + UncheckedIOException e = assertVoidThrows(UncheckedIOException.class, + () -> d.decode(data, true, nopCallback())); + assertNotNull(e.getCause()); + assertEquals(e.getCause().getClass(), ProtocolException.class); + assertExceptionMessageContains(e, "Unexpected end of header block"); + } + + // 5.2. String Literal Representation + // ...A Huffman-encoded string literal containing the EOS symbol MUST be + // treated as a decoding error... + @Test + public void corruptedHeaderBlockHuffmanStringEOS() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10000110, // huffman=true, length=6 + 0b00011001, 0b01001101, (byte) 0b11111111, + (byte) 0b11111111, (byte) 0b11111111, (byte) 0b11111100 + }); + IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "Encountered EOS"); + } + + // 5.2. String Literal Representation + // ...A padding strictly longer than 7 bits MUST be treated as a decoding + // error... + @Test + public void corruptedHeaderBlockHuffmanStringLongPadding1() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10000011, // huffman=true, length=3 + 0b00011001, 0b01001101, (byte) 0b11111111 + // len("aei") + len(padding) = (5 + 5 + 5) + (9) + }); + IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "Padding is too long", "len=9"); + } + + @Test + public void corruptedHeaderBlockHuffmanStringLongPadding2() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10000011, // huffman=true, length=3 + 0b00011001, 0b01111010, (byte) 0b11111111 + // len("aek") + len(padding) = (5 + 5 + 7) + (7) + }); + assertVoidDoesNotThrow(() -> d.decode(data, true, nopCallback())); + } + + // 5.2. String Literal Representation + // ...A padding not corresponding to the most significant bits of the code + // for the EOS symbol MUST be treated as a decoding error... + @Test + public void corruptedHeaderBlockHuffmanStringNotEOSPadding() { + Decoder d = new Decoder(4096); + ByteBuffer data = ByteBuffer.wrap(new byte[]{ + 0b00001111, // literal, index=15 + 0b00000000, + (byte) 0b10000011, // huffman=true, length=3 + 0b00011001, 0b01111010, (byte) 0b11111110 + }); + IllegalArgumentException e = assertVoidThrows(IllegalArgumentException.class, + () -> d.decode(data, true, nopCallback())); + + assertExceptionMessageContains(e, "Not a EOS prefix"); + } + + @Test + public void argsTestBiConsumerIsNull() { + Decoder decoder = new Decoder(4096); + assertVoidThrows(NullPointerException.class, + () -> decoder.decode(ByteBuffer.allocate(16), true, null)); + } + + @Test + public void argsTestByteBufferIsNull() { + Decoder decoder = new Decoder(4096); + assertVoidThrows(NullPointerException.class, + () -> decoder.decode(null, true, nopCallback())); + } + + @Test + public void argsTestBothAreNull() { + Decoder decoder = new Decoder(4096); + assertVoidThrows(NullPointerException.class, + () -> decoder.decode(null, true, null)); + } + + private static void test(String hexdump, + String headerTable, String headerList) { + test(new Decoder(4096), hexdump, headerTable, headerList); + } + + // + // Sometimes we need to keep the same decoder along several runs, + // as it models the same connection + // + private static void test(Decoder d, String hexdump, + String expectedHeaderTable, String expectedHeaderList) { + + ByteBuffer source = SpecHelper.toBytes(hexdump); + + List<String> actual = new LinkedList<>(); + d.decode(source, true, (name, value) -> { + if (value == null) { + actual.add(name.toString()); + } else { + actual.add(name + ": " + value); + } + }); + + assertEquals(d.getTable().getStateString(), expectedHeaderTable); + assertEquals(actual.stream().collect(Collectors.joining("\n")), expectedHeaderList); + } + + private static DecodingCallback nopCallback() { + return (t, u) -> { }; + } +} diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/EncoderTest.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/EncoderTest.java new file mode 100644 index 00000000000..c7b375b1238 --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/EncoderTest.java @@ -0,0 +1,623 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import static java.util.Arrays.asList; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static sun.net.httpclient.hpack.SpecHelper.toHexdump; +import static sun.net.httpclient.hpack.TestHelper.assertVoidThrows; + +// TODO: map textual representation of commands from the spec to actual +// calls to encoder (actually, this is a good idea for decoder as well) +public final class EncoderTest { + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.1 + // + @Test + public void example1() { + + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + e.literalWithIndexing("custom-key", false, "custom-header", false); + // @formatter:off + test(e, + + "400a 6375 7374 6f6d 2d6b 6579 0d63 7573\n" + + "746f 6d2d 6865 6164 6572", + + "[ 1] (s = 55) custom-key: custom-header\n" + + " Table size: 55"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.2 + // + @Test + public void example2() { + + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + e.literal(4, "/sample/path", false); + // @formatter:off + test(e, + + "040c 2f73 616d 706c 652f 7061 7468", + + "empty."); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.3 + // + @Test + public void example3() { + + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + e.literalNeverIndexed("password", false, "secret", false); + // @formatter:off + test(e, + + "1008 7061 7373 776f 7264 0673 6563 7265\n" + + "74", + + "empty."); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.2.4 + // + @Test + public void example4() { + + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + e.indexed(2); + // @formatter:off + test(e, + + "82", + + "empty."); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.3 + // + @Test + public void example5() { + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + ByteBuffer output = ByteBuffer.allocate(64); + e.indexed(2); + e.encode(output); + e.indexed(6); + e.encode(output); + e.indexed(4); + e.encode(output); + e.literalWithIndexing(1, "www.example.com", false); + e.encode(output); + + output.flip(); + + // @formatter:off + test(e, output, + + "8286 8441 0f77 7777 2e65 7861 6d70 6c65\n" + + "2e63 6f6d", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57"); + + output.clear(); + + e.indexed( 2); + e.encode(output); + e.indexed( 6); + e.encode(output); + e.indexed( 4); + e.encode(output); + e.indexed(62); + e.encode(output); + e.literalWithIndexing(24, "no-cache", false); + e.encode(output); + + output.flip(); + + test(e, output, + + "8286 84be 5808 6e6f 2d63 6163 6865", + + "[ 1] (s = 53) cache-control: no-cache\n" + + "[ 2] (s = 57) :authority: www.example.com\n" + + " Table size: 110"); + + output.clear(); + + e.indexed( 2); + e.encode(output); + e.indexed( 7); + e.encode(output); + e.indexed( 5); + e.encode(output); + e.indexed(63); + e.encode(output); + e.literalWithIndexing("custom-key", false, "custom-value", false); + e.encode(output); + + output.flip(); + + test(e, output, + + "8287 85bf 400a 6375 7374 6f6d 2d6b 6579\n" + + "0c63 7573 746f 6d2d 7661 6c75 65", + + "[ 1] (s = 54) custom-key: custom-value\n" + + "[ 2] (s = 53) cache-control: no-cache\n" + + "[ 3] (s = 57) :authority: www.example.com\n" + + " Table size: 164"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.4 + // + @Test + public void example6() { + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + ByteBuffer output = ByteBuffer.allocate(64); + e.indexed(2); + e.encode(output); + e.indexed(6); + e.encode(output); + e.indexed(4); + e.encode(output); + e.literalWithIndexing(1, "www.example.com", true); + e.encode(output); + + output.flip(); + + // @formatter:off + test(e, output, + + "8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4\n" + + "ff", + + "[ 1] (s = 57) :authority: www.example.com\n" + + " Table size: 57"); + + output.clear(); + + e.indexed( 2); + e.encode(output); + e.indexed( 6); + e.encode(output); + e.indexed( 4); + e.encode(output); + e.indexed(62); + e.encode(output); + e.literalWithIndexing(24, "no-cache", true); + e.encode(output); + + output.flip(); + + test(e, output, + + "8286 84be 5886 a8eb 1064 9cbf", + + "[ 1] (s = 53) cache-control: no-cache\n" + + "[ 2] (s = 57) :authority: www.example.com\n" + + " Table size: 110"); + + output.clear(); + + e.indexed( 2); + e.encode(output); + e.indexed( 7); + e.encode(output); + e.indexed( 5); + e.encode(output); + e.indexed(63); + e.encode(output); + e.literalWithIndexing("custom-key", true, "custom-value", true); + e.encode(output); + + output.flip(); + + test(e, output, + + "8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925\n" + + "a849 e95b b8e8 b4bf", + + "[ 1] (s = 54) custom-key: custom-value\n" + + "[ 2] (s = 53) cache-control: no-cache\n" + + "[ 3] (s = 57) :authority: www.example.com\n" + + " Table size: 164"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.5 + // + @Test + public void example7() { + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + ByteBuffer output = ByteBuffer.allocate(128); + // @formatter:off + e.literalWithIndexing( 8, "302", false); + e.encode(output); + e.literalWithIndexing(24, "private", false); + e.encode(output); + e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", false); + e.encode(output); + e.literalWithIndexing(46, "https://www.example.com", false); + e.encode(output); + + output.flip(); + + test(e, output, + + "4803 3330 3258 0770 7269 7661 7465 611d\n" + + "4d6f 6e2c 2032 3120 4f63 7420 3230 3133\n" + + "2032 303a 3133 3a32 3120 474d 546e 1768\n" + + "7474 7073 3a2f 2f77 7777 2e65 7861 6d70\n" + + "6c65 2e63 6f6d", + + "[ 1] (s = 63) location: https://www.example.com\n" + + "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 3] (s = 52) cache-control: private\n" + + "[ 4] (s = 42) :status: 302\n" + + " Table size: 222"); + + output.clear(); + + e.literalWithIndexing( 8, "307", false); + e.encode(output); + e.indexed(65); + e.encode(output); + e.indexed(64); + e.encode(output); + e.indexed(63); + e.encode(output); + + output.flip(); + + test(e, output, + + "4803 3330 37c1 c0bf", + + "[ 1] (s = 42) :status: 307\n" + + "[ 2] (s = 63) location: https://www.example.com\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 4] (s = 52) cache-control: private\n" + + " Table size: 222"); + + output.clear(); + + e.indexed( 8); + e.encode(output); + e.indexed(65); + e.encode(output); + e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", false); + e.encode(output); + e.indexed(64); + e.encode(output); + e.literalWithIndexing(26, "gzip", false); + e.encode(output); + e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", false); + e.encode(output); + + output.flip(); + + test(e, output, + + "88c1 611d 4d6f 6e2c 2032 3120 4f63 7420\n" + + "3230 3133 2032 303a 3133 3a32 3220 474d\n" + + "54c0 5a04 677a 6970 7738 666f 6f3d 4153\n" + + "444a 4b48 514b 425a 584f 5157 454f 5049\n" + + "5541 5851 5745 4f49 553b 206d 6178 2d61\n" + + "6765 3d33 3630 303b 2076 6572 7369 6f6e\n" + + "3d31", + + "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + + "[ 2] (s = 52) content-encoding: gzip\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + " Table size: 215"); + // @formatter:on + } + + // + // http://tools.ietf.org/html/rfc7541#appendix-C.6 + // + @Test + public void example8() { + Encoder e = newCustomEncoder(256); + drainInitialUpdate(e); + + ByteBuffer output = ByteBuffer.allocate(128); + // @formatter:off + e.literalWithIndexing( 8, "302", true); + e.encode(output); + e.literalWithIndexing(24, "private", true); + e.encode(output); + e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:21 GMT", true); + e.encode(output); + e.literalWithIndexing(46, "https://www.example.com", true); + e.encode(output); + + output.flip(); + + test(e, output, + + "4882 6402 5885 aec3 771a 4b61 96d0 7abe\n" + + "9410 54d4 44a8 2005 9504 0b81 66e0 82a6\n" + + "2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8\n" + + "e9ae 82ae 43d3", + + "[ 1] (s = 63) location: https://www.example.com\n" + + "[ 2] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 3] (s = 52) cache-control: private\n" + + "[ 4] (s = 42) :status: 302\n" + + " Table size: 222"); + + output.clear(); + + e.literalWithIndexing( 8, "307", true); + e.encode(output); + e.indexed(65); + e.encode(output); + e.indexed(64); + e.encode(output); + e.indexed(63); + e.encode(output); + + output.flip(); + + test(e, output, + + "4883 640e ffc1 c0bf", + + "[ 1] (s = 42) :status: 307\n" + + "[ 2] (s = 63) location: https://www.example.com\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:21 GMT\n" + + "[ 4] (s = 52) cache-control: private\n" + + " Table size: 222"); + + output.clear(); + + e.indexed( 8); + e.encode(output); + e.indexed(65); + e.encode(output); + e.literalWithIndexing(33, "Mon, 21 Oct 2013 20:13:22 GMT", true); + e.encode(output); + e.indexed(64); + e.encode(output); + e.literalWithIndexing(26, "gzip", true); + e.encode(output); + e.literalWithIndexing(55, "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", true); + e.encode(output); + + output.flip(); + + test(e, output, + + "88c1 6196 d07a be94 1054 d444 a820 0595\n" + + "040b 8166 e084 a62d 1bff c05a 839b d9ab\n" + + "77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b\n" + + "3960 d5af 2708 7f36 72c1 ab27 0fb5 291f\n" + + "9587 3160 65c0 03ed 4ee5 b106 3d50 07", + + "[ 1] (s = 98) set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1\n" + + "[ 2] (s = 52) content-encoding: gzip\n" + + "[ 3] (s = 65) date: Mon, 21 Oct 2013 20:13:22 GMT\n" + + " Table size: 215"); + // @formatter:on + } + + @Test + public void initialSizeUpdateDefaultEncoder() { + Function<Integer, Encoder> e = Encoder::new; + testSizeUpdate(e, 1024, asList(), asList(0)); + testSizeUpdate(e, 1024, asList(1024), asList(0)); + testSizeUpdate(e, 1024, asList(1024, 1024), asList(0)); + testSizeUpdate(e, 1024, asList(1024, 512), asList(0)); + testSizeUpdate(e, 1024, asList(512, 1024), asList(0)); + testSizeUpdate(e, 1024, asList(512, 2048), asList(0)); + } + + @Test + public void initialSizeUpdateCustomEncoder() { + Function<Integer, Encoder> e = EncoderTest::newCustomEncoder; + testSizeUpdate(e, 1024, asList(), asList(1024)); + testSizeUpdate(e, 1024, asList(1024), asList(1024)); + testSizeUpdate(e, 1024, asList(1024, 1024), asList(1024)); + testSizeUpdate(e, 1024, asList(1024, 512), asList(512)); + testSizeUpdate(e, 1024, asList(512, 1024), asList(1024)); + testSizeUpdate(e, 1024, asList(512, 2048), asList(2048)); + } + + @Test + public void seriesOfSizeUpdatesDefaultEncoder() { + Function<Integer, Encoder> e = c -> { + Encoder encoder = new Encoder(c); + drainInitialUpdate(encoder); + return encoder; + }; + testSizeUpdate(e, 0, asList(0), asList()); + testSizeUpdate(e, 1024, asList(1024), asList()); + testSizeUpdate(e, 1024, asList(2048), asList()); + testSizeUpdate(e, 1024, asList(512), asList()); + testSizeUpdate(e, 1024, asList(1024, 1024), asList()); + testSizeUpdate(e, 1024, asList(1024, 2048), asList()); + testSizeUpdate(e, 1024, asList(2048, 1024), asList()); + testSizeUpdate(e, 1024, asList(1024, 512), asList()); + testSizeUpdate(e, 1024, asList(512, 1024), asList()); + } + + // + // https://tools.ietf.org/html/rfc7541#section-4.2 + // + @Test + public void seriesOfSizeUpdatesCustomEncoder() { + Function<Integer, Encoder> e = c -> { + Encoder encoder = newCustomEncoder(c); + drainInitialUpdate(encoder); + return encoder; + }; + testSizeUpdate(e, 0, asList(0), asList()); + testSizeUpdate(e, 1024, asList(1024), asList()); + testSizeUpdate(e, 1024, asList(2048), asList(2048)); + testSizeUpdate(e, 1024, asList(512), asList(512)); + testSizeUpdate(e, 1024, asList(1024, 1024), asList()); + testSizeUpdate(e, 1024, asList(1024, 2048), asList(2048)); + testSizeUpdate(e, 1024, asList(2048, 1024), asList()); + testSizeUpdate(e, 1024, asList(1024, 512), asList(512)); + testSizeUpdate(e, 1024, asList(512, 1024), asList(512, 1024)); + } + + @Test + public void callSequenceViolations() { + { // Hasn't set up a header + Encoder e = new Encoder(0); + assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); + } + { // Can't set up header while there's an unfinished encoding + Encoder e = new Encoder(0); + e.indexed(32); + assertVoidThrows(IllegalStateException.class, () -> e.indexed(32)); + } + { // Can't setMaxCapacity while there's an unfinished encoding + Encoder e = new Encoder(0); + e.indexed(32); + assertVoidThrows(IllegalStateException.class, () -> e.setMaxCapacity(512)); + } + { // Hasn't set up a header + Encoder e = new Encoder(0); + e.setMaxCapacity(256); + assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); + } + { // Hasn't set up a header after the previous encoding + Encoder e = new Encoder(0); + e.indexed(0); + boolean encoded = e.encode(ByteBuffer.allocate(16)); + assertTrue(encoded); // assumption + assertVoidThrows(IllegalStateException.class, () -> e.encode(ByteBuffer.allocate(16))); + } + } + + private static void test(Encoder encoder, + String expectedTableState, + String expectedHexdump) { + + ByteBuffer b = ByteBuffer.allocate(128); + encoder.encode(b); + b.flip(); + test(encoder, b, expectedTableState, expectedHexdump); + } + + private static void test(Encoder encoder, + ByteBuffer output, + String expectedHexdump, + String expectedTableState) { + + String actualTableState = encoder.getHeaderTable().getStateString(); + assertEquals(actualTableState, expectedTableState); + + String actualHexdump = toHexdump(output); + assertEquals(actualHexdump, expectedHexdump.replaceAll("\\n", " ")); + } + + // initial size - the size encoder is constructed with + // updates - a sequence of values for consecutive calls to encoder.setMaxCapacity + // expected - a sequence of values expected to be decoded by a decoder + private void testSizeUpdate(Function<Integer, Encoder> encoder, + int initialSize, + List<Integer> updates, + List<Integer> expected) { + Encoder e = encoder.apply(initialSize); + updates.forEach(e::setMaxCapacity); + ByteBuffer b = ByteBuffer.allocate(64); + e.header("a", "b"); + e.encode(b); + b.flip(); + Decoder d = new Decoder(updates.isEmpty() ? initialSize : Collections.max(updates)); + List<Integer> actual = new ArrayList<>(); + d.decode(b, true, new DecodingCallback() { + @Override + public void onDecoded(CharSequence name, CharSequence value) { } + + @Override + public void onSizeUpdate(int capacity) { + actual.add(capacity); + } + }); + assertEquals(actual, expected); + } + + // + // Default encoder does not need any table, therefore a subclass that + // behaves differently is needed + // + private static Encoder newCustomEncoder(int maxCapacity) { + return new Encoder(maxCapacity) { + @Override + protected int calculateCapacity(int maxCapacity) { + return maxCapacity; + } + }; + } + + private static void drainInitialUpdate(Encoder e) { + ByteBuffer b = ByteBuffer.allocate(4); + e.header("a", "b"); + boolean done; + do { + done = e.encode(b); + b.flip(); + } while (!done); + } +} diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/HeaderTableTest.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/HeaderTableTest.java new file mode 100644 index 00000000000..1bc12029ca6 --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/HeaderTableTest.java @@ -0,0 +1,375 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import org.testng.annotations.Test; +import sun.net.httpclient.hpack.HeaderTable.HeaderField; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.String.format; +import static org.testng.Assert.assertEquals; +import static sun.net.httpclient.hpack.TestHelper.*; + +public class HeaderTableTest { + + // + // https://tools.ietf.org/html/rfc7541#appendix-A + // + // @formatter:off + private static final String SPEC = + " | 1 | :authority | |\n" + + " | 2 | :method | GET |\n" + + " | 3 | :method | POST |\n" + + " | 4 | :path | / |\n" + + " | 5 | :path | /index.html |\n" + + " | 6 | :scheme | http |\n" + + " | 7 | :scheme | https |\n" + + " | 8 | :status | 200 |\n" + + " | 9 | :status | 204 |\n" + + " | 10 | :status | 206 |\n" + + " | 11 | :status | 304 |\n" + + " | 12 | :status | 400 |\n" + + " | 13 | :status | 404 |\n" + + " | 14 | :status | 500 |\n" + + " | 15 | accept-charset | |\n" + + " | 16 | accept-encoding | gzip, deflate |\n" + + " | 17 | accept-language | |\n" + + " | 18 | accept-ranges | |\n" + + " | 19 | accept | |\n" + + " | 20 | access-control-allow-origin | |\n" + + " | 21 | age | |\n" + + " | 22 | allow | |\n" + + " | 23 | authorization | |\n" + + " | 24 | cache-control | |\n" + + " | 25 | content-disposition | |\n" + + " | 26 | content-encoding | |\n" + + " | 27 | content-language | |\n" + + " | 28 | content-length | |\n" + + " | 29 | content-location | |\n" + + " | 30 | content-range | |\n" + + " | 31 | content-type | |\n" + + " | 32 | cookie | |\n" + + " | 33 | date | |\n" + + " | 34 | etag | |\n" + + " | 35 | expect | |\n" + + " | 36 | expires | |\n" + + " | 37 | from | |\n" + + " | 38 | host | |\n" + + " | 39 | if-match | |\n" + + " | 40 | if-modified-since | |\n" + + " | 41 | if-none-match | |\n" + + " | 42 | if-range | |\n" + + " | 43 | if-unmodified-since | |\n" + + " | 44 | last-modified | |\n" + + " | 45 | link | |\n" + + " | 46 | location | |\n" + + " | 47 | max-forwards | |\n" + + " | 48 | proxy-authenticate | |\n" + + " | 49 | proxy-authorization | |\n" + + " | 50 | range | |\n" + + " | 51 | referer | |\n" + + " | 52 | refresh | |\n" + + " | 53 | retry-after | |\n" + + " | 54 | server | |\n" + + " | 55 | set-cookie | |\n" + + " | 56 | strict-transport-security | |\n" + + " | 57 | transfer-encoding | |\n" + + " | 58 | user-agent | |\n" + + " | 59 | vary | |\n" + + " | 60 | via | |\n" + + " | 61 | www-authenticate | |\n"; + // @formatter:on + + private static final int STATIC_TABLE_LENGTH = createStaticEntries().size(); + private final Random rnd = newRandom(); + + @Test + public void staticData() { + HeaderTable table = new HeaderTable(0); + Map<Integer, HeaderField> staticHeaderFields = createStaticEntries(); + + Map<String, Integer> minimalIndexes = new HashMap<>(); + + for (Map.Entry<Integer, HeaderField> e : staticHeaderFields.entrySet()) { + Integer idx = e.getKey(); + String hName = e.getValue().name; + Integer midx = minimalIndexes.get(hName); + if (midx == null) { + minimalIndexes.put(hName, idx); + } else { + minimalIndexes.put(hName, Math.min(idx, midx)); + } + } + + staticHeaderFields.entrySet().forEach( + e -> { + // lookup + HeaderField actualHeaderField = table.get(e.getKey()); + HeaderField expectedHeaderField = e.getValue(); + assertEquals(actualHeaderField, expectedHeaderField); + + // reverse lookup (name, value) + String hName = expectedHeaderField.name; + String hValue = expectedHeaderField.value; + int expectedIndex = e.getKey(); + int actualIndex = table.indexOf(hName, hValue); + + assertEquals(actualIndex, expectedIndex); + + // reverse lookup (name) + int expectedMinimalIndex = minimalIndexes.get(hName); + int actualMinimalIndex = table.indexOf(hName, "blah-blah"); + + assertEquals(-actualMinimalIndex, expectedMinimalIndex); + } + ); + } + + @Test + public void constructorSetsMaxSize() { + int size = rnd.nextInt(64); + HeaderTable t = new HeaderTable(size); + assertEquals(t.size(), 0); + assertEquals(t.maxSize(), size); + } + + @Test + public void negativeMaximumSize() { + int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1] + IllegalArgumentException e = + assertVoidThrows(IllegalArgumentException.class, + () -> new HeaderTable(0).setMaxSize(maxSize)); + assertExceptionMessageContains(e, "maxSize"); + } + + @Test + public void zeroMaximumSize() { + HeaderTable table = new HeaderTable(0); + table.setMaxSize(0); + assertEquals(table.maxSize(), 0); + } + + @Test + public void negativeIndex() { + int idx = -(rnd.nextInt(256) + 1); // [-256, -1] + IllegalArgumentException e = + assertVoidThrows(IllegalArgumentException.class, + () -> new HeaderTable(0).get(idx)); + assertExceptionMessageContains(e, "index"); + } + + @Test + public void zeroIndex() { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, + () -> new HeaderTable(0).get(0)); + assertExceptionMessageContains(e, "index"); + } + + @Test + public void length() { + HeaderTable table = new HeaderTable(0); + assertEquals(table.length(), STATIC_TABLE_LENGTH); + } + + @Test + public void indexOutsideStaticRange() { + HeaderTable table = new HeaderTable(0); + int idx = table.length() + (rnd.nextInt(256) + 1); + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, + () -> table.get(idx)); + assertExceptionMessageContains(e, "index"); + } + + @Test + public void entryPutAfterStaticArea() { + HeaderTable table = new HeaderTable(256); + int idx = table.length() + 1; + assertThrows(IllegalArgumentException.class, () -> table.get(idx)); + + byte[] bytes = new byte[32]; + rnd.nextBytes(bytes); + String name = new String(bytes, StandardCharsets.ISO_8859_1); + String value = "custom-value"; + + table.put(name, value); + HeaderField f = table.get(idx); + assertEquals(name, f.name); + assertEquals(value, f.value); + } + + @Test + public void staticTableHasZeroSize() { + HeaderTable table = new HeaderTable(0); + assertEquals(0, table.size()); + } + + @Test + public void lowerIndexPriority() { + HeaderTable table = new HeaderTable(256); + int oldLength = table.length(); + table.put("bender", "rodriguez"); + table.put("bender", "rodriguez"); + table.put("bender", "rodriguez"); + + assertEquals(table.length(), oldLength + 3); // more like an assumption + int i = table.indexOf("bender", "rodriguez"); + assertEquals(oldLength + 1, i); + } + + @Test + public void lowerIndexPriority2() { + HeaderTable table = new HeaderTable(256); + int oldLength = table.length(); + int idx = rnd.nextInt(oldLength) + 1; + HeaderField f = table.get(idx); + table.put(f.name, f.value); + assertEquals(table.length(), oldLength + 1); + int i = table.indexOf(f.name, f.value); + assertEquals(idx, i); + } + + // TODO: negative indexes check + // TODO: ensure full table clearance when adding huge header field + // TODO: ensure eviction deletes minimum needed entries, not more + + @Test + public void fifo() { + HeaderTable t = new HeaderTable(Integer.MAX_VALUE); + // Let's add a series of header fields + int NUM_HEADERS = 32; + for (int i = 1; i <= NUM_HEADERS; i++) { + String s = String.valueOf(i); + t.put(s, s); + } + // They MUST appear in a FIFO order: + // newer entries are at lower indexes + // older entries are at higher indexes + for (int j = 1; j <= NUM_HEADERS; j++) { + HeaderField f = t.get(STATIC_TABLE_LENGTH + j); + int actualName = Integer.parseInt(f.name); + int expectedName = NUM_HEADERS - j + 1; + assertEquals(expectedName, actualName); + } + // Entries MUST be evicted in the order they were added: + // the newer the entry the later it is evicted + for (int k = 1; k <= NUM_HEADERS; k++) { + HeaderField f = t.evictEntry(); + assertEquals(String.valueOf(k), f.name); + } + } + + @Test + public void indexOf() { + HeaderTable t = new HeaderTable(Integer.MAX_VALUE); + // Let's put a series of header fields + int NUM_HEADERS = 32; + for (int i = 1; i <= NUM_HEADERS; i++) { + String s = String.valueOf(i); + t.put(s, s); + } + // and verify indexOf (reverse lookup) returns correct indexes for + // full lookup + for (int j = 1; j <= NUM_HEADERS; j++) { + String s = String.valueOf(j); + int actualIndex = t.indexOf(s, s); + int expectedIndex = STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1; + assertEquals(expectedIndex, actualIndex); + } + // as well as for just a name lookup + for (int j = 1; j <= NUM_HEADERS; j++) { + String s = String.valueOf(j); + int actualIndex = t.indexOf(s, "blah"); + int expectedIndex = -(STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1); + assertEquals(expectedIndex, actualIndex); + } + // lookup for non-existent name returns 0 + assertEquals(0, t.indexOf("chupacabra", "1")); + } + + @Test + public void testToString() { + HeaderTable table = new HeaderTable(0); + { + table.setMaxSize(2048); + assertEquals("entries: 0; used 0/2048 (0.0%)", table.toString()); + } + + { + String name = "custom-name"; + String value = "custom-value"; + int size = 512; + + table.setMaxSize(size); + table.put(name, value); + String s = table.toString(); + + int used = name.length() + value.length() + 32; + double ratio = used * 100.0 / size; + + String expected = format("entries: 1; used %s/%s (%.1f%%)", used, size, ratio); + assertEquals(expected, s); + } + + { + table.setMaxSize(78); + table.put(":method", ""); + table.put(":status", ""); + String s = table.toString(); + assertEquals("entries: 2; used 78/78 (100.0%)", s); + } + } + + @Test + public void stateString() { + HeaderTable table = new HeaderTable(256); + table.put("custom-key", "custom-header"); + // @formatter:off + assertEquals("[ 1] (s = 55) custom-key: custom-header\n" + + " Table size: 55", table.getStateString()); + // @formatter:on + } + + private static Map<Integer, HeaderField> createStaticEntries() { + Pattern line = Pattern.compile( + "\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|"); + Matcher m = line.matcher(SPEC); + Map<Integer, HeaderField> result = new HashMap<>(); + while (m.find()) { + int index = Integer.parseInt(m.group("index")); + String name = m.group("name"); + String value = m.group("value"); + HeaderField f = new HeaderField(name, value); + result.put(index, f); + } + return Collections.unmodifiableMap(result); // lol + } +} diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/HuffmanTest.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/HuffmanTest.java new file mode 100644 index 00000000000..502c7051a79 --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/HuffmanTest.java @@ -0,0 +1,623 @@ +/* + * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import org.testng.annotations.Test; + +import java.nio.ByteBuffer; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.lang.Integer.parseInt; +import static org.testng.Assert.*; + +public final class HuffmanTest { + + // + // https://tools.ietf.org/html/rfc7541#appendix-B + // + private static final String SPEC = + // @formatter:off + " code as bits as hex len\n" + + " sym aligned to MSB aligned in\n" + + " to LSB bits\n" + + " ( 0) |11111111|11000 1ff8 [13]\n" + + " ( 1) |11111111|11111111|1011000 7fffd8 [23]\n" + + " ( 2) |11111111|11111111|11111110|0010 fffffe2 [28]\n" + + " ( 3) |11111111|11111111|11111110|0011 fffffe3 [28]\n" + + " ( 4) |11111111|11111111|11111110|0100 fffffe4 [28]\n" + + " ( 5) |11111111|11111111|11111110|0101 fffffe5 [28]\n" + + " ( 6) |11111111|11111111|11111110|0110 fffffe6 [28]\n" + + " ( 7) |11111111|11111111|11111110|0111 fffffe7 [28]\n" + + " ( 8) |11111111|11111111|11111110|1000 fffffe8 [28]\n" + + " ( 9) |11111111|11111111|11101010 ffffea [24]\n" + + " ( 10) |11111111|11111111|11111111|111100 3ffffffc [30]\n" + + " ( 11) |11111111|11111111|11111110|1001 fffffe9 [28]\n" + + " ( 12) |11111111|11111111|11111110|1010 fffffea [28]\n" + + " ( 13) |11111111|11111111|11111111|111101 3ffffffd [30]\n" + + " ( 14) |11111111|11111111|11111110|1011 fffffeb [28]\n" + + " ( 15) |11111111|11111111|11111110|1100 fffffec [28]\n" + + " ( 16) |11111111|11111111|11111110|1101 fffffed [28]\n" + + " ( 17) |11111111|11111111|11111110|1110 fffffee [28]\n" + + " ( 18) |11111111|11111111|11111110|1111 fffffef [28]\n" + + " ( 19) |11111111|11111111|11111111|0000 ffffff0 [28]\n" + + " ( 20) |11111111|11111111|11111111|0001 ffffff1 [28]\n" + + " ( 21) |11111111|11111111|11111111|0010 ffffff2 [28]\n" + + " ( 22) |11111111|11111111|11111111|111110 3ffffffe [30]\n" + + " ( 23) |11111111|11111111|11111111|0011 ffffff3 [28]\n" + + " ( 24) |11111111|11111111|11111111|0100 ffffff4 [28]\n" + + " ( 25) |11111111|11111111|11111111|0101 ffffff5 [28]\n" + + " ( 26) |11111111|11111111|11111111|0110 ffffff6 [28]\n" + + " ( 27) |11111111|11111111|11111111|0111 ffffff7 [28]\n" + + " ( 28) |11111111|11111111|11111111|1000 ffffff8 [28]\n" + + " ( 29) |11111111|11111111|11111111|1001 ffffff9 [28]\n" + + " ( 30) |11111111|11111111|11111111|1010 ffffffa [28]\n" + + " ( 31) |11111111|11111111|11111111|1011 ffffffb [28]\n" + + " ' ' ( 32) |010100 14 [ 6]\n" + + " '!' ( 33) |11111110|00 3f8 [10]\n" + + " '\"' ( 34) |11111110|01 3f9 [10]\n" + + " '#' ( 35) |11111111|1010 ffa [12]\n" + + " '$' ( 36) |11111111|11001 1ff9 [13]\n" + + " '%' ( 37) |010101 15 [ 6]\n" + + " '&' ( 38) |11111000 f8 [ 8]\n" + + " ''' ( 39) |11111111|010 7fa [11]\n" + + " '(' ( 40) |11111110|10 3fa [10]\n" + + " ')' ( 41) |11111110|11 3fb [10]\n" + + " '*' ( 42) |11111001 f9 [ 8]\n" + + " '+' ( 43) |11111111|011 7fb [11]\n" + + " ',' ( 44) |11111010 fa [ 8]\n" + + " '-' ( 45) |010110 16 [ 6]\n" + + " '.' ( 46) |010111 17 [ 6]\n" + + " '/' ( 47) |011000 18 [ 6]\n" + + " '0' ( 48) |00000 0 [ 5]\n" + + " '1' ( 49) |00001 1 [ 5]\n" + + " '2' ( 50) |00010 2 [ 5]\n" + + " '3' ( 51) |011001 19 [ 6]\n" + + " '4' ( 52) |011010 1a [ 6]\n" + + " '5' ( 53) |011011 1b [ 6]\n" + + " '6' ( 54) |011100 1c [ 6]\n" + + " '7' ( 55) |011101 1d [ 6]\n" + + " '8' ( 56) |011110 1e [ 6]\n" + + " '9' ( 57) |011111 1f [ 6]\n" + + " ':' ( 58) |1011100 5c [ 7]\n" + + " ';' ( 59) |11111011 fb [ 8]\n" + + " '<' ( 60) |11111111|1111100 7ffc [15]\n" + + " '=' ( 61) |100000 20 [ 6]\n" + + " '>' ( 62) |11111111|1011 ffb [12]\n" + + " '?' ( 63) |11111111|00 3fc [10]\n" + + " '@' ( 64) |11111111|11010 1ffa [13]\n" + + " 'A' ( 65) |100001 21 [ 6]\n" + + " 'B' ( 66) |1011101 5d [ 7]\n" + + " 'C' ( 67) |1011110 5e [ 7]\n" + + " 'D' ( 68) |1011111 5f [ 7]\n" + + " 'E' ( 69) |1100000 60 [ 7]\n" + + " 'F' ( 70) |1100001 61 [ 7]\n" + + " 'G' ( 71) |1100010 62 [ 7]\n" + + " 'H' ( 72) |1100011 63 [ 7]\n" + + " 'I' ( 73) |1100100 64 [ 7]\n" + + " 'J' ( 74) |1100101 65 [ 7]\n" + + " 'K' ( 75) |1100110 66 [ 7]\n" + + " 'L' ( 76) |1100111 67 [ 7]\n" + + " 'M' ( 77) |1101000 68 [ 7]\n" + + " 'N' ( 78) |1101001 69 [ 7]\n" + + " 'O' ( 79) |1101010 6a [ 7]\n" + + " 'P' ( 80) |1101011 6b [ 7]\n" + + " 'Q' ( 81) |1101100 6c [ 7]\n" + + " 'R' ( 82) |1101101 6d [ 7]\n" + + " 'S' ( 83) |1101110 6e [ 7]\n" + + " 'T' ( 84) |1101111 6f [ 7]\n" + + " 'U' ( 85) |1110000 70 [ 7]\n" + + " 'V' ( 86) |1110001 71 [ 7]\n" + + " 'W' ( 87) |1110010 72 [ 7]\n" + + " 'X' ( 88) |11111100 fc [ 8]\n" + + " 'Y' ( 89) |1110011 73 [ 7]\n" + + " 'Z' ( 90) |11111101 fd [ 8]\n" + + " '[' ( 91) |11111111|11011 1ffb [13]\n" + + " '\\' ( 92) |11111111|11111110|000 7fff0 [19]\n" + + " ']' ( 93) |11111111|11100 1ffc [13]\n" + + " '^' ( 94) |11111111|111100 3ffc [14]\n" + + " '_' ( 95) |100010 22 [ 6]\n" + + " '`' ( 96) |11111111|1111101 7ffd [15]\n" + + " 'a' ( 97) |00011 3 [ 5]\n" + + " 'b' ( 98) |100011 23 [ 6]\n" + + " 'c' ( 99) |00100 4 [ 5]\n" + + " 'd' (100) |100100 24 [ 6]\n" + + " 'e' (101) |00101 5 [ 5]\n" + + " 'f' (102) |100101 25 [ 6]\n" + + " 'g' (103) |100110 26 [ 6]\n" + + " 'h' (104) |100111 27 [ 6]\n" + + " 'i' (105) |00110 6 [ 5]\n" + + " 'j' (106) |1110100 74 [ 7]\n" + + " 'k' (107) |1110101 75 [ 7]\n" + + " 'l' (108) |101000 28 [ 6]\n" + + " 'm' (109) |101001 29 [ 6]\n" + + " 'n' (110) |101010 2a [ 6]\n" + + " 'o' (111) |00111 7 [ 5]\n" + + " 'p' (112) |101011 2b [ 6]\n" + + " 'q' (113) |1110110 76 [ 7]\n" + + " 'r' (114) |101100 2c [ 6]\n" + + " 's' (115) |01000 8 [ 5]\n" + + " 't' (116) |01001 9 [ 5]\n" + + " 'u' (117) |101101 2d [ 6]\n" + + " 'v' (118) |1110111 77 [ 7]\n" + + " 'w' (119) |1111000 78 [ 7]\n" + + " 'x' (120) |1111001 79 [ 7]\n" + + " 'y' (121) |1111010 7a [ 7]\n" + + " 'z' (122) |1111011 7b [ 7]\n" + + " '{' (123) |11111111|1111110 7ffe [15]\n" + + " '|' (124) |11111111|100 7fc [11]\n" + + " '}' (125) |11111111|111101 3ffd [14]\n" + + " '~' (126) |11111111|11101 1ffd [13]\n" + + " (127) |11111111|11111111|11111111|1100 ffffffc [28]\n" + + " (128) |11111111|11111110|0110 fffe6 [20]\n" + + " (129) |11111111|11111111|010010 3fffd2 [22]\n" + + " (130) |11111111|11111110|0111 fffe7 [20]\n" + + " (131) |11111111|11111110|1000 fffe8 [20]\n" + + " (132) |11111111|11111111|010011 3fffd3 [22]\n" + + " (133) |11111111|11111111|010100 3fffd4 [22]\n" + + " (134) |11111111|11111111|010101 3fffd5 [22]\n" + + " (135) |11111111|11111111|1011001 7fffd9 [23]\n" + + " (136) |11111111|11111111|010110 3fffd6 [22]\n" + + " (137) |11111111|11111111|1011010 7fffda [23]\n" + + " (138) |11111111|11111111|1011011 7fffdb [23]\n" + + " (139) |11111111|11111111|1011100 7fffdc [23]\n" + + " (140) |11111111|11111111|1011101 7fffdd [23]\n" + + " (141) |11111111|11111111|1011110 7fffde [23]\n" + + " (142) |11111111|11111111|11101011 ffffeb [24]\n" + + " (143) |11111111|11111111|1011111 7fffdf [23]\n" + + " (144) |11111111|11111111|11101100 ffffec [24]\n" + + " (145) |11111111|11111111|11101101 ffffed [24]\n" + + " (146) |11111111|11111111|010111 3fffd7 [22]\n" + + " (147) |11111111|11111111|1100000 7fffe0 [23]\n" + + " (148) |11111111|11111111|11101110 ffffee [24]\n" + + " (149) |11111111|11111111|1100001 7fffe1 [23]\n" + + " (150) |11111111|11111111|1100010 7fffe2 [23]\n" + + " (151) |11111111|11111111|1100011 7fffe3 [23]\n" + + " (152) |11111111|11111111|1100100 7fffe4 [23]\n" + + " (153) |11111111|11111110|11100 1fffdc [21]\n" + + " (154) |11111111|11111111|011000 3fffd8 [22]\n" + + " (155) |11111111|11111111|1100101 7fffe5 [23]\n" + + " (156) |11111111|11111111|011001 3fffd9 [22]\n" + + " (157) |11111111|11111111|1100110 7fffe6 [23]\n" + + " (158) |11111111|11111111|1100111 7fffe7 [23]\n" + + " (159) |11111111|11111111|11101111 ffffef [24]\n" + + " (160) |11111111|11111111|011010 3fffda [22]\n" + + " (161) |11111111|11111110|11101 1fffdd [21]\n" + + " (162) |11111111|11111110|1001 fffe9 [20]\n" + + " (163) |11111111|11111111|011011 3fffdb [22]\n" + + " (164) |11111111|11111111|011100 3fffdc [22]\n" + + " (165) |11111111|11111111|1101000 7fffe8 [23]\n" + + " (166) |11111111|11111111|1101001 7fffe9 [23]\n" + + " (167) |11111111|11111110|11110 1fffde [21]\n" + + " (168) |11111111|11111111|1101010 7fffea [23]\n" + + " (169) |11111111|11111111|011101 3fffdd [22]\n" + + " (170) |11111111|11111111|011110 3fffde [22]\n" + + " (171) |11111111|11111111|11110000 fffff0 [24]\n" + + " (172) |11111111|11111110|11111 1fffdf [21]\n" + + " (173) |11111111|11111111|011111 3fffdf [22]\n" + + " (174) |11111111|11111111|1101011 7fffeb [23]\n" + + " (175) |11111111|11111111|1101100 7fffec [23]\n" + + " (176) |11111111|11111111|00000 1fffe0 [21]\n" + + " (177) |11111111|11111111|00001 1fffe1 [21]\n" + + " (178) |11111111|11111111|100000 3fffe0 [22]\n" + + " (179) |11111111|11111111|00010 1fffe2 [21]\n" + + " (180) |11111111|11111111|1101101 7fffed [23]\n" + + " (181) |11111111|11111111|100001 3fffe1 [22]\n" + + " (182) |11111111|11111111|1101110 7fffee [23]\n" + + " (183) |11111111|11111111|1101111 7fffef [23]\n" + + " (184) |11111111|11111110|1010 fffea [20]\n" + + " (185) |11111111|11111111|100010 3fffe2 [22]\n" + + " (186) |11111111|11111111|100011 3fffe3 [22]\n" + + " (187) |11111111|11111111|100100 3fffe4 [22]\n" + + " (188) |11111111|11111111|1110000 7ffff0 [23]\n" + + " (189) |11111111|11111111|100101 3fffe5 [22]\n" + + " (190) |11111111|11111111|100110 3fffe6 [22]\n" + + " (191) |11111111|11111111|1110001 7ffff1 [23]\n" + + " (192) |11111111|11111111|11111000|00 3ffffe0 [26]\n" + + " (193) |11111111|11111111|11111000|01 3ffffe1 [26]\n" + + " (194) |11111111|11111110|1011 fffeb [20]\n" + + " (195) |11111111|11111110|001 7fff1 [19]\n" + + " (196) |11111111|11111111|100111 3fffe7 [22]\n" + + " (197) |11111111|11111111|1110010 7ffff2 [23]\n" + + " (198) |11111111|11111111|101000 3fffe8 [22]\n" + + " (199) |11111111|11111111|11110110|0 1ffffec [25]\n" + + " (200) |11111111|11111111|11111000|10 3ffffe2 [26]\n" + + " (201) |11111111|11111111|11111000|11 3ffffe3 [26]\n" + + " (202) |11111111|11111111|11111001|00 3ffffe4 [26]\n" + + " (203) |11111111|11111111|11111011|110 7ffffde [27]\n" + + " (204) |11111111|11111111|11111011|111 7ffffdf [27]\n" + + " (205) |11111111|11111111|11111001|01 3ffffe5 [26]\n" + + " (206) |11111111|11111111|11110001 fffff1 [24]\n" + + " (207) |11111111|11111111|11110110|1 1ffffed [25]\n" + + " (208) |11111111|11111110|010 7fff2 [19]\n" + + " (209) |11111111|11111111|00011 1fffe3 [21]\n" + + " (210) |11111111|11111111|11111001|10 3ffffe6 [26]\n" + + " (211) |11111111|11111111|11111100|000 7ffffe0 [27]\n" + + " (212) |11111111|11111111|11111100|001 7ffffe1 [27]\n" + + " (213) |11111111|11111111|11111001|11 3ffffe7 [26]\n" + + " (214) |11111111|11111111|11111100|010 7ffffe2 [27]\n" + + " (215) |11111111|11111111|11110010 fffff2 [24]\n" + + " (216) |11111111|11111111|00100 1fffe4 [21]\n" + + " (217) |11111111|11111111|00101 1fffe5 [21]\n" + + " (218) |11111111|11111111|11111010|00 3ffffe8 [26]\n" + + " (219) |11111111|11111111|11111010|01 3ffffe9 [26]\n" + + " (220) |11111111|11111111|11111111|1101 ffffffd [28]\n" + + " (221) |11111111|11111111|11111100|011 7ffffe3 [27]\n" + + " (222) |11111111|11111111|11111100|100 7ffffe4 [27]\n" + + " (223) |11111111|11111111|11111100|101 7ffffe5 [27]\n" + + " (224) |11111111|11111110|1100 fffec [20]\n" + + " (225) |11111111|11111111|11110011 fffff3 [24]\n" + + " (226) |11111111|11111110|1101 fffed [20]\n" + + " (227) |11111111|11111111|00110 1fffe6 [21]\n" + + " (228) |11111111|11111111|101001 3fffe9 [22]\n" + + " (229) |11111111|11111111|00111 1fffe7 [21]\n" + + " (230) |11111111|11111111|01000 1fffe8 [21]\n" + + " (231) |11111111|11111111|1110011 7ffff3 [23]\n" + + " (232) |11111111|11111111|101010 3fffea [22]\n" + + " (233) |11111111|11111111|101011 3fffeb [22]\n" + + " (234) |11111111|11111111|11110111|0 1ffffee [25]\n" + + " (235) |11111111|11111111|11110111|1 1ffffef [25]\n" + + " (236) |11111111|11111111|11110100 fffff4 [24]\n" + + " (237) |11111111|11111111|11110101 fffff5 [24]\n" + + " (238) |11111111|11111111|11111010|10 3ffffea [26]\n" + + " (239) |11111111|11111111|1110100 7ffff4 [23]\n" + + " (240) |11111111|11111111|11111010|11 3ffffeb [26]\n" + + " (241) |11111111|11111111|11111100|110 7ffffe6 [27]\n" + + " (242) |11111111|11111111|11111011|00 3ffffec [26]\n" + + " (243) |11111111|11111111|11111011|01 3ffffed [26]\n" + + " (244) |11111111|11111111|11111100|111 7ffffe7 [27]\n" + + " (245) |11111111|11111111|11111101|000 7ffffe8 [27]\n" + + " (246) |11111111|11111111|11111101|001 7ffffe9 [27]\n" + + " (247) |11111111|11111111|11111101|010 7ffffea [27]\n" + + " (248) |11111111|11111111|11111101|011 7ffffeb [27]\n" + + " (249) |11111111|11111111|11111111|1110 ffffffe [28]\n" + + " (250) |11111111|11111111|11111101|100 7ffffec [27]\n" + + " (251) |11111111|11111111|11111101|101 7ffffed [27]\n" + + " (252) |11111111|11111111|11111101|110 7ffffee [27]\n" + + " (253) |11111111|11111111|11111101|111 7ffffef [27]\n" + + " (254) |11111111|11111111|11111110|000 7fffff0 [27]\n" + + " (255) |11111111|11111111|11111011|10 3ffffee [26]\n" + + " EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]"; + // @formatter:on + + @Test + public void read_table() { + Pattern line = Pattern.compile( + "\\(\\s*(?<ascii>\\d+)\\s*\\)\\s*(?<binary>(\\|(0|1)+)+)\\s*" + + "(?<hex>[0-9a-zA-Z]+)\\s*\\[\\s*(?<len>\\d+)\\s*\\]"); + Matcher m = line.matcher(SPEC); + int i = 0; + while (m.find()) { + String ascii = m.group("ascii"); + String binary = m.group("binary").replaceAll("\\|", ""); + String hex = m.group("hex"); + String len = m.group("len"); + + // Several sanity checks for the data read from the table, just to + // make sure what we read makes sense + assertEquals(parseInt(len), binary.length()); + assertEquals(parseInt(binary, 2), parseInt(hex, 16)); + + int expected = parseInt(ascii); + + // TODO: find actual eos, do not hardcode it! + byte[] bytes = intToBytes(0x3fffffff, 30, + parseInt(hex, 16), parseInt(len)); + + StringBuilder actual = new StringBuilder(); + Huffman.Reader t = new Huffman.Reader(); + t.read(ByteBuffer.wrap(bytes), actual, false, true); + + // What has been read MUST represent a single symbol + assertEquals(actual.length(), 1, "ascii: " + ascii); + + // It's a lot more visual to compare char as codes rather than + // characters (as some of them might not be visible) + assertEquals(actual.charAt(0), expected); + i++; + } + assertEquals(i, 257); // 256 + EOS + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.4.1 + // + @Test + public void read_1() { + read("f1e3 c2e5 f23a 6ba0 ab90 f4ff", "www.example.com"); + } + + @Test + public void write_1() { + write("www.example.com", "f1e3 c2e5 f23a 6ba0 ab90 f4ff"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.4.2 + // + @Test + public void read_2() { + read("a8eb 1064 9cbf", "no-cache"); + } + + @Test + public void write_2() { + write("no-cache", "a8eb 1064 9cbf"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.4.3 + // + @Test + public void read_3() { + read("25a8 49e9 5ba9 7d7f", "custom-key"); + } + + @Test + public void write_3() { + write("custom-key", "25a8 49e9 5ba9 7d7f"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.4.3 + // + @Test + public void read_4() { + read("25a8 49e9 5bb8 e8b4 bf", "custom-value"); + } + + @Test + public void write_4() { + write("custom-value", "25a8 49e9 5bb8 e8b4 bf"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 + // + @Test + public void read_5() { + read("6402", "302"); + } + + @Test + public void write_5() { + write("302", "6402"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 + // + @Test + public void read_6() { + read("aec3 771a 4b", "private"); + } + + @Test + public void write_6() { + write("private", "aec3 771a 4b"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 + // + @Test + public void read_7() { + read("d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff", + "Mon, 21 Oct 2013 20:13:21 GMT"); + } + + @Test + public void write_7() { + write("Mon, 21 Oct 2013 20:13:21 GMT", + "d07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.1 + // + @Test + public void read_8() { + read("9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3", + "https://www.example.com"); + } + + @Test + public void write_8() { + write("https://www.example.com", + "9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.2 + // + @Test + public void read_9() { + read("640e ff", "307"); + } + + @Test + public void write_9() { + write("307", "640e ff"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 + // + @Test + public void read_10() { + read("d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff", + "Mon, 21 Oct 2013 20:13:22 GMT"); + } + + @Test + public void write_10() { + write("Mon, 21 Oct 2013 20:13:22 GMT", + "d07a be94 1054 d444 a820 0595 040b 8166 e084 a62d 1bff"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 + // + @Test + public void read_11() { + read("9bd9 ab", "gzip"); + } + + @Test + public void write_11() { + write("gzip", "9bd9 ab"); + } + + // + // https://tools.ietf.org/html/rfc7541#appendix-C.6.3 + // + @Test + public void read_12() { + read("94e7 821d d7f2 e6c7 b335 dfdf cd5b 3960 " + + "d5af 2708 7f36 72c1 ab27 0fb5 291f 9587 " + + "3160 65c0 03ed 4ee5 b106 3d50 07", + "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"); + } + + @Test + public void test_trie_has_no_empty_nodes() { + Huffman.Node root = Huffman.INSTANCE.getRoot(); + Stack<Huffman.Node> backlog = new Stack<>(); + backlog.push(root); + while (!backlog.isEmpty()) { + Huffman.Node n = backlog.pop(); + // The only type of nodes we couldn't possibly catch during + // construction is an empty node: no children and no char + if (n.left != null) { + backlog.push(n.left); + } + if (n.right != null) { + backlog.push(n.right); + } + assertFalse(!n.charIsSet && n.left == null && n.right == null, + "Empty node in the trie"); + } + } + + @Test + public void test_trie_has_257_nodes() { + int count = 0; + Huffman.Node root = Huffman.INSTANCE.getRoot(); + Stack<Huffman.Node> backlog = new Stack<>(); + backlog.push(root); + while (!backlog.isEmpty()) { + Huffman.Node n = backlog.pop(); + if (n.left != null) { + backlog.push(n.left); + } + if (n.right != null) { + backlog.push(n.right); + } + if (n.isLeaf()) { + count++; + } + } + assertEquals(count, 257); + } + + @Test + public void cant_encode_outside_byte() { + TestHelper.Block<Object> coding = + () -> new Huffman.Writer() + .from(((char) 256) + "", 0, 1) + .write(ByteBuffer.allocate(1)); + RuntimeException e = + TestHelper.assertVoidThrows(RuntimeException.class, coding); + TestHelper.assertExceptionMessageContains(e, "char"); + } + + private static void read(String hexdump, String decoded) { + ByteBuffer source = SpecHelper.toBytes(hexdump); + Appendable actual = new StringBuilder(); + new Huffman.Reader().read(source, actual, true); + assertEquals(actual.toString(), decoded); + } + + private static void write(String decoded, String hexdump) { + int n = Huffman.INSTANCE.lengthOf(decoded); + ByteBuffer destination = ByteBuffer.allocate(n); // Extra margin (1) to test having more bytes in the destination than needed is ok + Huffman.Writer writer = new Huffman.Writer(); + BuffersTestingKit.forEachSplit(destination, byteBuffers -> { + writer.from(decoded, 0, decoded.length()); + boolean written = false; + for (ByteBuffer b : byteBuffers) { + int pos = b.position(); + written = writer.write(b); + b.position(pos); + } + assertTrue(written); + ByteBuffer concated = BuffersTestingKit.concat(byteBuffers); + String actual = SpecHelper.toHexdump(concated); + assertEquals(actual, hexdump); + writer.reset(); + }); + } + + // + // It's not very pretty, yes I know that + // + // hex: + // + // |31|30|...|N-1|...|01|00| + // \ / + // codeLength + // + // hex <<= 32 - codeLength; (align to MSB): + // + // |31|30|...|32-N|...|01|00| + // \ / + // codeLength + // + // EOS: + // + // |31|30|...|M-1|...|01|00| + // \ / + // eosLength + // + // eos <<= 32 - eosLength; (align to MSB): + // + // pad with MSBs of EOS: + // + // |31|30|...|32-N|32-N-1|...|01|00| + // | 32|...| + // + // Finally, split into byte[] + // + private byte[] intToBytes(int eos, int eosLength, int hex, int codeLength) { + hex <<= 32 - codeLength; + eos >>= codeLength - (32 - eosLength); + hex |= eos; + int n = (int) Math.ceil(codeLength / 8.0); + byte[] result = new byte[n]; + for (int i = 0; i < n; i++) { + result[i] = (byte) (hex >> (32 - 8 * (i + 1))); + } + return result; + } +} diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/SpecHelper.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/SpecHelper.java new file mode 100644 index 00000000000..88bd906514e --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/SpecHelper.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +// +// THIS IS NOT A TEST +// +public final class SpecHelper { + + private SpecHelper() { + throw new AssertionError(); + } + + public static ByteBuffer toBytes(String hexdump) { + Pattern hexByte = Pattern.compile("[0-9a-fA-F]{2}"); + List<String> bytes = new ArrayList<>(); + Matcher matcher = hexByte.matcher(hexdump); + while (matcher.find()) { + bytes.add(matcher.group(0)); + } + ByteBuffer result = ByteBuffer.allocate(bytes.size()); + for (String f : bytes) { + result.put((byte) Integer.parseInt(f, 16)); + } + result.flip(); + return result; + } + + public static String toHexdump(ByteBuffer bb) { + List<String> words = new ArrayList<>(); + int i = 0; + while (bb.hasRemaining()) { + if (i % 2 == 0) { + words.add(""); + } + byte b = bb.get(); + String hex = Integer.toHexString(256 + Byte.toUnsignedInt(b)).substring(1); + words.set(i / 2, words.get(i / 2) + hex); + i++; + } + return words.stream().collect(Collectors.joining(" ")); + } +} diff --git a/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/TestHelper.java b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/TestHelper.java new file mode 100644 index 00000000000..b042b5f4cee --- /dev/null +++ b/jdk/test/java/net/httpclient/http2/java.httpclient/sun/net/httpclient/hpack/TestHelper.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package sun.net.httpclient.hpack; + +import org.testng.annotations.Test; + +import java.util.Objects; +import java.util.Random; + +public final class TestHelper { + + public static Random newRandom() { + long seed = Long.getLong("jdk.test.lib.random.seed", System.currentTimeMillis()); + System.out.println("new java.util.Random(" + seed + ")"); + return new Random(seed); + } + + public static <T extends Throwable> T assertVoidThrows(Class<T> clazz, Block<?> code) { + return assertThrows(clazz, () -> { + code.run(); + return null; + }); + } + + public static <T extends Throwable> T assertThrows(Class<T> clazz, ReturningBlock<?> code) { + Objects.requireNonNull(clazz, "clazz == null"); + Objects.requireNonNull(code, "code == null"); + try { + code.run(); + } catch (Throwable t) { + if (clazz.isInstance(t)) { + return clazz.cast(t); + } + throw new AssertionError("Expected to catch exception of type " + + clazz.getCanonicalName() + ", instead caught " + + t.getClass().getCanonicalName(), t); + + } + throw new AssertionError( + "Expected to catch exception of type " + clazz.getCanonicalName() + + ", but caught nothing"); + } + + public static <T> T assertDoesNotThrow(ReturningBlock<T> code) { + Objects.requireNonNull(code, "code == null"); + try { + return code.run(); + } catch (Throwable t) { + throw new AssertionError( + "Expected code block to exit normally, instead " + + "caught " + t.getClass().getCanonicalName(), t); + } + } + + public static void assertVoidDoesNotThrow(Block<?> code) { + Objects.requireNonNull(code, "code == null"); + try { + code.run(); + } catch (Throwable t) { + throw new AssertionError( + "Expected code block to exit normally, instead " + + "caught " + t.getClass().getCanonicalName(), t); + } + } + + + public static void assertExceptionMessageContains(Throwable t, + CharSequence firstSubsequence, + CharSequence... others) { + assertCharSequenceContains(t.getMessage(), firstSubsequence, others); + } + + public static void assertCharSequenceContains(CharSequence s, + CharSequence firstSubsequence, + CharSequence... others) { + if (s == null) { + throw new NullPointerException("Exception message is null"); + } + String str = s.toString(); + String missing = null; + if (!str.contains(firstSubsequence.toString())) { + missing = firstSubsequence.toString(); + } else { + for (CharSequence o : others) { + if (!str.contains(o.toString())) { + missing = o.toString(); + break; + } + } + } + if (missing != null) { + throw new AssertionError("CharSequence '" + s + "'" + " does not " + + "contain subsequence '" + missing + "'"); + } + } + + public interface ReturningBlock<T> { + T run() throws Throwable; + } + + public interface Block<T> { + void run() throws Throwable; + } + + // tests + + @Test + public void assertThrows() { + assertThrows(NullPointerException.class, () -> ((Object) null).toString()); + } + + @Test + public void assertThrowsWrongType() { + try { + assertThrows(IllegalArgumentException.class, () -> ((Object) null).toString()); + } catch (AssertionError e) { + Throwable cause = e.getCause(); + String message = e.getMessage(); + if (cause != null + && cause instanceof NullPointerException + && message != null + && message.contains("instead caught")) { + return; + } + } + throw new AssertionError(); + } + + @Test + public void assertThrowsNoneCaught() { + try { + assertThrows(IllegalArgumentException.class, () -> null); + } catch (AssertionError e) { + Throwable cause = e.getCause(); + String message = e.getMessage(); + if (cause == null + && message != null + && message.contains("but caught nothing")) { + return; + } + } + throw new AssertionError(); + } +}