From 883e4f79c29e598cdd766a33f7d2821ca3db471b Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Fri, 12 Feb 2021 20:18:24 +1100 Subject: [PATCH] Implement Encoder & Decoder Streams to encode instructions. Signed-off-by: Lachlan Roberts --- .../jetty/http3/qpack/DecoderStream.java | 57 ++++++++- .../jetty/http3/qpack/EncoderStream.java | 120 +++++++++++++++++- .../DecoderInstructionParser.java | 2 +- .../EncoderInstructionParser.java | 4 +- .../qpack/{ => parser}/NBitIntegerParser.java | 2 +- .../qpack/{ => parser}/NBitStringParser.java | 5 +- .../jetty/http3/qpack/DecoderStreamTest.java | 40 ++++++ .../jetty/http3/qpack/EncoderStreamTest.java | 40 ++++++ .../qpack/IncomingDecoderStreamTest.java | 1 + .../qpack/IncomingEncoderStreamTest.java | 1 + .../http3/qpack/NBitIntegerParserTest.java | 1 + 11 files changed, 260 insertions(+), 13 deletions(-) rename jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/{ => parser}/DecoderInstructionParser.java (98%) rename jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/{ => parser}/EncoderInstructionParser.java (98%) rename jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/{ => parser}/NBitIntegerParser.java (97%) rename jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/{ => parser}/NBitStringParser.java (94%) create mode 100644 jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/DecoderStreamTest.java create mode 100644 jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/EncoderStreamTest.java diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/DecoderStream.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/DecoderStream.java index 2b4fbb42f62..cf7a29a60d3 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/DecoderStream.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/DecoderStream.java @@ -13,11 +13,60 @@ package org.eclipse.jetty.http3.qpack; -public interface DecoderStream +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.NullByteBufferPool; +import org.eclipse.jetty.util.BufferUtil; + +public class DecoderStream { - void sendSectionAcknowledgment(int streamId); + private final OutputStream _outputStream; + private final ByteBufferPool _bufferPool; - void sendStreamCancellation(int streamId); + public DecoderStream(OutputStream outputStream) + { + this (outputStream, new NullByteBufferPool()); + } - void sendInsertCountIncrement(int increment); + public DecoderStream(OutputStream outputStream, ByteBufferPool bufferPool) + { + _outputStream = outputStream; + _bufferPool = bufferPool; + } + + void sendSectionAcknowledgment(int streamId) throws IOException + { + int size = NBitInteger.octectsNeeded(7, streamId) + 1; + ByteBuffer buffer = _bufferPool.acquire(size, false); + BufferUtil.clearToFill(buffer); + buffer.put((byte)0x80); + NBitInteger.encode(buffer, 7, streamId); + BufferUtil.flipToFlush(buffer, 0); + BufferUtil.writeTo(buffer, _outputStream); + } + + void sendStreamCancellation(int streamId) throws IOException + { + int size = NBitInteger.octectsNeeded(6, streamId) + 1; + ByteBuffer buffer = _bufferPool.acquire(size, false); + BufferUtil.clearToFill(buffer); + buffer.put((byte)0x40); + NBitInteger.encode(buffer, 6, streamId); + BufferUtil.flipToFlush(buffer, 0); + BufferUtil.writeTo(buffer, _outputStream); + } + + void sendInsertCountIncrement(int increment) throws IOException + { + int size = NBitInteger.octectsNeeded(6, increment) + 1; + ByteBuffer buffer = _bufferPool.acquire(size, false); + BufferUtil.clearToFill(buffer); + buffer.put((byte)0x00); + NBitInteger.encode(buffer, 6, increment); + BufferUtil.flipToFlush(buffer, 0); + BufferUtil.writeTo(buffer, _outputStream); + } } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncoderStream.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncoderStream.java index ec78f1d99fc..4b03f05f2a1 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncoderStream.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncoderStream.java @@ -13,13 +13,123 @@ package org.eclipse.jetty.http3.qpack; -public interface EncoderStream +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.NullByteBufferPool; +import org.eclipse.jetty.util.BufferUtil; + +public class EncoderStream { - void setCapacity(int capacity); + private final OutputStream _outputStream; + private final ByteBufferPool _bufferPool; - void insertEntry(String name, String value); + public EncoderStream(OutputStream outputStream) + { + this (outputStream, new NullByteBufferPool()); + } - void insertEntry(int nameRef, String value); + public EncoderStream(OutputStream outputStream, ByteBufferPool bufferPool) + { + _outputStream = outputStream; + _bufferPool = bufferPool; + } - void insertEntry(int ref); + private boolean shouldHuffmanEncode(String s) + { + return s.length() > 128; + } + + void setCapacity(int capacity) throws IOException + { + int size = NBitInteger.octectsNeeded(5, capacity) + 1; + ByteBuffer buffer = _bufferPool.acquire(size, false); + BufferUtil.clearToFill(buffer); + buffer.put((byte)0x20); + NBitInteger.encode(buffer, 5, capacity); + BufferUtil.flipToFlush(buffer, 0); + BufferUtil.writeTo(buffer, _outputStream); + } + + void insertEntry(String name, String value) throws IOException + { + boolean huffmanEncodeName = shouldHuffmanEncode(name); + boolean huffmanEncodeValue = shouldHuffmanEncode(value); + + int size = (huffmanEncodeName ? Huffman.octetsNeeded(name) : name.length()) + + (huffmanEncodeValue ? Huffman.octetsNeeded(value) : value.length()) + 2; + ByteBuffer buffer = _bufferPool.acquire(size, false); + BufferUtil.clearToFill(buffer); + + if (huffmanEncodeName) + { + buffer.put((byte)(0x40 | 0x20)); + NBitInteger.encode(buffer, 5, Huffman.octetsNeeded(name)); + Huffman.encode(buffer, name); + } + else + { + buffer.put((byte)(0x40)); + NBitInteger.encode(buffer, 5, name.length()); + buffer.put(name.getBytes()); + } + + if (huffmanEncodeValue) + { + buffer.put((byte)(0x80)); + NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(value)); + Huffman.encode(buffer, value); + } + else + { + buffer.put((byte)(0x00)); + NBitInteger.encode(buffer, 5, value.length()); + buffer.put(value.getBytes()); + } + + BufferUtil.flipToFlush(buffer, 0); + BufferUtil.writeTo(buffer, _outputStream); + } + + void insertEntry(int nameRef, boolean dynamicTableReference, String value) throws IOException + { + boolean huffmanEncode = shouldHuffmanEncode(value); + int size = NBitInteger.octectsNeeded(6, nameRef) + (huffmanEncode ? Huffman.octetsNeeded(value) : value.length()) + 2; + ByteBuffer buffer = _bufferPool.acquire(size, false); + BufferUtil.clearToFill(buffer); + + // First bit indicates the instruction, second bit is whether it is a dynamic table reference or not. + buffer.put((byte)(0x80 | (dynamicTableReference ? 0x00 : 0x40))); + NBitInteger.encode(buffer, 6, nameRef); + + // We will not huffman encode the string. + if (huffmanEncode) + { + buffer.put((byte)(0x80)); + NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(value)); + Huffman.encode(buffer, value); + } + else + { + buffer.put((byte)(0x00)); + NBitInteger.encode(buffer, 7, value.length()); + buffer.put(value.getBytes()); + } + + BufferUtil.flipToFlush(buffer, 0); + BufferUtil.writeTo(buffer, _outputStream); + } + + void insertEntry(int ref) throws IOException + { + int size = NBitInteger.octectsNeeded(5, ref) + 1; + ByteBuffer buffer = _bufferPool.acquire(size, false); + BufferUtil.clearToFill(buffer); + buffer.put((byte)0x00); + NBitInteger.encode(buffer, 5, ref); + BufferUtil.flipToFlush(buffer, 0); + BufferUtil.writeTo(buffer, _outputStream); + } } diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/DecoderInstructionParser.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/DecoderInstructionParser.java similarity index 98% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/DecoderInstructionParser.java rename to jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/DecoderInstructionParser.java index 2af3de341f3..a15dd48a044 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/DecoderInstructionParser.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/DecoderInstructionParser.java @@ -11,7 +11,7 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack; +package org.eclipse.jetty.http3.qpack.parser; import java.nio.ByteBuffer; diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncoderInstructionParser.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/EncoderInstructionParser.java similarity index 98% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncoderInstructionParser.java rename to jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/EncoderInstructionParser.java index a7eb1aa2809..7ea2d9bee61 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/EncoderInstructionParser.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/EncoderInstructionParser.java @@ -11,10 +11,12 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack; +package org.eclipse.jetty.http3.qpack.parser; import java.nio.ByteBuffer; +import org.eclipse.jetty.http3.qpack.QpackException; + /** * Receives instructions coming from the remote Decoder as a sequence of unframed instructions. */ diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/NBitIntegerParser.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/NBitIntegerParser.java similarity index 97% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/NBitIntegerParser.java rename to jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/NBitIntegerParser.java index 514847e1b7c..e2c6b8d4880 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/NBitIntegerParser.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/NBitIntegerParser.java @@ -11,7 +11,7 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack; +package org.eclipse.jetty.http3.qpack.parser; import java.nio.ByteBuffer; diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/NBitStringParser.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/NBitStringParser.java similarity index 94% rename from jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/NBitStringParser.java rename to jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/NBitStringParser.java index ede27806c88..1e7d630de33 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/NBitStringParser.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/parser/NBitStringParser.java @@ -11,10 +11,13 @@ // ======================================================================== // -package org.eclipse.jetty.http3.qpack; +package org.eclipse.jetty.http3.qpack.parser; import java.nio.ByteBuffer; +import org.eclipse.jetty.http3.qpack.HuffmanDecoder; +import org.eclipse.jetty.http3.qpack.QpackException; + public class NBitStringParser { private final NBitIntegerParser _integerParser; diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/DecoderStreamTest.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/DecoderStreamTest.java new file mode 100644 index 00000000000..04331e5f516 --- /dev/null +++ b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/DecoderStreamTest.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http3.qpack; + +import java.io.ByteArrayOutputStream; + +import org.eclipse.jetty.util.TypeUtil; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalToIgnoringCase; + +public class DecoderStreamTest +{ + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private final DecoderStream decoderStream = new DecoderStream(outputStream); + + @Test + public void test() throws Exception + { + decoderStream.sendSectionAcknowledgment(4); + assertThat(TypeUtil.toHexString(outputStream.toByteArray()), equalToIgnoringCase("84")); + outputStream.reset(); + + decoderStream.sendSectionAcknowledgment(1337); + assertThat(TypeUtil.toHexString(outputStream.toByteArray()), equalToIgnoringCase("FFBA09")); + outputStream.reset(); + } +} diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/EncoderStreamTest.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/EncoderStreamTest.java new file mode 100644 index 00000000000..aa90adcfbb2 --- /dev/null +++ b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/EncoderStreamTest.java @@ -0,0 +1,40 @@ +// +// ======================================================================== +// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http3.qpack; + +import java.io.ByteArrayOutputStream; + +import org.eclipse.jetty.util.TypeUtil; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalToIgnoringCase; + +public class EncoderStreamTest +{ + private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private final EncoderStream encoderStream = new EncoderStream(outputStream); + + @Test + public void test() throws Exception + { + encoderStream.insertEntry(0, false, "www.example.com"); + assertThat(TypeUtil.toHexString(outputStream.toByteArray()), equalToIgnoringCase("c00f7777772e6578616d706c652e636f6d")); + outputStream.reset(); + + encoderStream.insertEntry(1, false, "/sample/path"); + assertThat(TypeUtil.toHexString(outputStream.toByteArray()), equalToIgnoringCase("c10c2f73616d706c652f70617468")); + outputStream.reset(); + } +} diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/IncomingDecoderStreamTest.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/IncomingDecoderStreamTest.java index 96e261bfc0f..372a4cef839 100644 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/IncomingDecoderStreamTest.java +++ b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/IncomingDecoderStreamTest.java @@ -17,6 +17,7 @@ import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.Queue; +import org.eclipse.jetty.http3.qpack.parser.EncoderInstructionParser; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; import org.junit.jupiter.api.BeforeEach; diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/IncomingEncoderStreamTest.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/IncomingEncoderStreamTest.java index 7716517045e..63f3363897b 100644 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/IncomingEncoderStreamTest.java +++ b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/IncomingEncoderStreamTest.java @@ -17,6 +17,7 @@ import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.Queue; +import org.eclipse.jetty.http3.qpack.parser.DecoderInstructionParser; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; import org.junit.jupiter.api.Test; diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerParserTest.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerParserTest.java index aa41876c0ff..0ae1d06da55 100644 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerParserTest.java +++ b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/NBitIntegerParserTest.java @@ -15,6 +15,7 @@ package org.eclipse.jetty.http3.qpack; import java.nio.ByteBuffer; +import org.eclipse.jetty.http3.qpack.parser.NBitIntegerParser; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; import org.junit.jupiter.api.Test;