Implement insertNameWithReference and insertWithLiteralName in EncoderInstructionParser.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2021-02-11 17:03:36 +11:00 committed by Simone Bordet
parent b3cd2a1c27
commit 2916b60091
5 changed files with 440 additions and 18 deletions

View File

@ -53,11 +53,14 @@ public class DecoderInstructionParser
public void parse(ByteBuffer buffer)
{
if (buffer == null || !buffer.hasRemaining())
return;
switch (_state)
{
case PARSING:
// Get first byte without incrementing the buffers position.
byte firstByte = buffer.slice().get();
byte firstByte = buffer.get(buffer.position());
if ((firstByte & 0x80) != 0)
{
_state = State.SECTION_ACKNOWLEDGEMENT;

View File

@ -21,8 +21,14 @@ import java.nio.ByteBuffer;
public class EncoderInstructionParser
{
private final Handler _handler;
private final NBitStringParser _stringParser;
private final NBitIntegerParser _integerParser;
private State _state = State.PARSING;
private Operation _operation = Operation.NONE;
private boolean _referenceDynamicTable;
private int _index;
private String _name;
private enum State
{
@ -33,25 +39,41 @@ public class EncoderInstructionParser
DUPLICATE
}
private enum Operation
{
NONE,
INDEX,
NAME,
VALUE,
}
public interface Handler
{
void onSetDynamicTableCapacity(int capacity);
void onDuplicate(int index);
void onInsertNameWithReference(int nameIndex, boolean isDynamicTableIndex, String value);
void onInsertWithLiteralName(String name, String value);
}
public EncoderInstructionParser(Handler handler)
{
_handler = handler;
_stringParser = new NBitStringParser();
_integerParser = new NBitIntegerParser();
}
public void parse(ByteBuffer buffer)
public void parse(ByteBuffer buffer) throws QpackException
{
if (buffer == null || !buffer.hasRemaining())
return;
switch (_state)
{
case PARSING:
byte firstByte = buffer.slice().get();
byte firstByte = buffer.get(buffer.position());
if ((firstByte & 0x80) != 0)
{
_state = State.REFERENCED_NAME;
@ -95,25 +117,76 @@ public class EncoderInstructionParser
}
}
private void parseInsertNameWithReference(ByteBuffer buffer)
private void parseInsertNameWithReference(ByteBuffer buffer) throws QpackException
{
// TODO
// Single bit boolean whether reference is to dynamic or static table.
// Index with 6-bit prefix.
// Single bit wither it is huffman encoded.
// Length of the encoded string.
// The string itself.
while (true)
{
switch (_operation)
{
case NONE:
byte firstByte = buffer.get(buffer.position());
_referenceDynamicTable = (firstByte & 0x40) == 0;
_operation = Operation.INDEX;
continue;
case INDEX:
_index = _integerParser.decode(buffer, 6);
if (_index < 0)
return;
_stringParser.setPrefix(8);
_operation = Operation.VALUE;
continue;
case VALUE:
String value = _stringParser.decode(buffer);
if (value == null)
return;
_operation = Operation.NONE;
_state = State.PARSING;
_handler.onInsertNameWithReference(_index, _referenceDynamicTable, value);
return;
default:
throw new IllegalStateException(_operation.name());
}
}
}
private void parseInsertWithLiteralName(ByteBuffer buffer)
private void parseInsertWithLiteralName(ByteBuffer buffer) throws QpackException
{
// TODO
// Single bit whether name is huffman encoded.
// Length of name with 5-bit prefix.
// Name bytes.
// Single bit whether value is huffman encoded.
// Value length with 7-bit prefix.
// Value bytes.
while (true)
{
switch (_operation)
{
case NONE:
_stringParser.setPrefix(6);
_operation = Operation.NAME;
continue;
case NAME:
_name = _stringParser.decode(buffer);
if (_name == null)
return;
_stringParser.setPrefix(8);
_operation = Operation.VALUE;
continue;
case VALUE:
String value = _stringParser.decode(buffer);
if (value == null)
return;
_operation = Operation.NONE;
_state = State.PARSING;
_handler.onInsertWithLiteralName(_name, value);
return;
default:
throw new IllegalStateException(_operation.name());
}
}
}
private void parseDuplicate(ByteBuffer buffer)

View File

@ -0,0 +1,108 @@
//
// ========================================================================
// 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.nio.ByteBuffer;
import org.eclipse.jetty.util.Utf8StringBuilder;
public class HuffmanDecoder
{
static final char EOS = Huffman.EOS;
static final char[] tree = Huffman.tree;
static final char[] rowsym = Huffman.rowsym;
static final byte[] rowbits = Huffman.rowbits;
private final Utf8StringBuilder utf8 = new Utf8StringBuilder();
private int count = 0;
private int node = 0;
private int current = 0;
private int bits = 0;
public String decode(ByteBuffer buffer, int length) throws QpackException.CompressionException
{
for (; count < length; count++)
{
if (!buffer.hasRemaining())
return null;
int b = buffer.get() & 0xFF;
current = (current << 8) | b;
bits += 8;
while (bits >= 8)
{
int c = (current >>> (bits - 8)) & 0xFF;
node = tree[node * 256 + c];
if (rowbits[node] != 0)
{
if (rowsym[node] == EOS)
{
reset();
throw new QpackException.CompressionException("EOS in content");
}
// terminal node
utf8.append((byte)(0xFF & rowsym[node]));
bits -= rowbits[node];
node = 0;
}
else
{
// non-terminal node
bits -= 8;
}
}
}
while (bits > 0)
{
int c = (current << (8 - bits)) & 0xFF;
int lastNode = node;
node = tree[node * 256 + c];
if (rowbits[node] == 0 || rowbits[node] > bits)
{
int requiredPadding = 0;
for (int i = 0; i < bits; i++)
{
requiredPadding = (requiredPadding << 1) | 1;
}
if ((c >> (8 - bits)) != requiredPadding)
throw new QpackException.CompressionException("Incorrect padding");
node = lastNode;
break;
}
utf8.append((byte)(0xFF & rowsym[node]));
bits -= rowbits[node];
node = 0;
}
if (node != 0)
throw new QpackException.CompressionException("Bad termination");
return utf8.toString();
}
public void reset()
{
utf8.reset();
count = 0;
current = 0;
node = 0;
bits = 0;
}
}

View File

@ -0,0 +1,102 @@
//
// ========================================================================
// 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.nio.ByteBuffer;
public class NBitStringParser
{
private final NBitIntegerParser _integerParser;
private final HuffmanDecoder _huffmanBuilder;
private final StringBuilder _stringBuilder;
private boolean _huffman;
private int _count;
private int _length;
private int _prefix;
private State _state = State.PARSING;
private enum State
{
PARSING,
LENGTH,
VALUE
}
public NBitStringParser()
{
_integerParser = new NBitIntegerParser();
_huffmanBuilder = new HuffmanDecoder();
_stringBuilder = new StringBuilder();
}
public void setPrefix(int prefix)
{
_prefix = prefix;
}
public String decode(ByteBuffer buffer) throws QpackException
{
while (true)
{
switch (_state)
{
case PARSING:
byte firstByte = buffer.get(buffer.position());
_huffman = ((0x80 >>> (8 - _prefix)) & firstByte) != 0;
_state = State.LENGTH;
continue;
case LENGTH:
_length = _integerParser.decode(buffer, _prefix - 1);
if (_length < 0)
return null;
_state = State.VALUE;
continue;
case VALUE:
String value = _huffman ? _huffmanBuilder.decode(buffer, _length) : asciiStringDecode(buffer);
if (value != null)
reset();
return value;
default:
throw new IllegalStateException(_state.name());
}
}
}
private String asciiStringDecode(ByteBuffer buffer)
{
for (; _count < _length; _count++)
{
if (!buffer.hasRemaining())
return null;
_stringBuilder.append((char)(0x7F & buffer.get()));
}
return _stringBuilder.toString();
}
public void reset()
{
_state = State.PARSING;
_integerParser.reset();
_huffmanBuilder.reset();
_stringBuilder.setLength(0);
_prefix = 0;
_count = 0;
_length = 0;
_huffman = false;
}
}

View File

@ -0,0 +1,136 @@
//
// ========================================================================
// 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.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Queue;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class IncomingDecoderStreamTest
{
public static class DebugHandler implements EncoderInstructionParser.Handler
{
public Queue<Integer> setCapacities = new LinkedList<>();
public Queue<Integer> duplicates = new LinkedList<>();
public Queue<Entry> literalNameEntries = new LinkedList<>();
public Queue<ReferencedEntry> referencedNameEntries = new LinkedList<>();
public static class Entry
{
public Entry(String name, String value)
{
this.value = value;
this.name = name;
}
String name;
String value;
}
public static class ReferencedEntry
{
public ReferencedEntry(int index, boolean dynamic, String value)
{
this.index = index;
this.dynamic = dynamic;
this.value = value;
}
int index;
boolean dynamic;
String value;
}
@Override
public void onSetDynamicTableCapacity(int capacity)
{
setCapacities.add(capacity);
}
@Override
public void onDuplicate(int index)
{
duplicates.add(index);
}
@Override
public void onInsertNameWithReference(int nameIndex, boolean isDynamicTableIndex, String value)
{
referencedNameEntries.add(new ReferencedEntry(nameIndex, isDynamicTableIndex, value));
}
@Override
public void onInsertWithLiteralName(String name, String value)
{
literalNameEntries.add(new Entry(name, value));
}
public boolean isEmpty()
{
return setCapacities.isEmpty() && duplicates.isEmpty() && literalNameEntries.isEmpty() && referencedNameEntries.isEmpty();
}
}
private EncoderInstructionParser _instructionParser;
private DebugHandler _handler;
@BeforeEach
public void before()
{
_handler = new DebugHandler();
_instructionParser = new EncoderInstructionParser(_handler);
}
@Test
public void testAddWithReferencedEntry() throws Exception
{
String insertAuthorityEntry = "c00f7777772e6578616d706c652e636f6d";
ByteBuffer buffer = BufferUtil.toBuffer(TypeUtil.fromHexString(insertAuthorityEntry));
_instructionParser.parse(buffer);
DebugHandler.ReferencedEntry entry = _handler.referencedNameEntries.poll();
assertNotNull(entry);
assertThat(entry.index, is(0));
assertThat(entry.dynamic, is(false));
assertThat(entry.value, is("www.example.com"));
String insertPathEntry = "c10c2f73616d706c652f70617468";
buffer = BufferUtil.toBuffer(TypeUtil.fromHexString(insertPathEntry));
_instructionParser.parse(buffer);
entry = _handler.referencedNameEntries.poll();
assertNotNull(entry);
assertThat(entry.index, is(1));
assertThat(entry.dynamic, is(false));
assertThat(entry.value, is("/sample/path"));
}
@Test
public void testSetCapacity() throws Exception
{
String setCapacityString = "3fbd01";
ByteBuffer buffer = BufferUtil.toBuffer(TypeUtil.fromHexString(setCapacityString));
_instructionParser.parse(buffer);
assertThat(_handler.setCapacities.poll(), is(220));
assertTrue(_handler.isEmpty());
}
}