blob: 179bfc616d79ad7c3a94d13a7c638860d3a2cab1 [file] [log] [blame]
/*
* 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 jdk.incubator.http.internal.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;
}
}