Work through examples B.1. and B.2. from spec and fix bugs.

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2021-03-03 12:03:17 +11:00 committed by Simone Bordet
parent 5b178d16b7
commit bdf44b8e22
13 changed files with 448 additions and 244 deletions

View File

@ -23,7 +23,8 @@ public enum HttpVersion
HTTP_0_9("HTTP/0.9", 9),
HTTP_1_0("HTTP/1.0", 10),
HTTP_1_1("HTTP/1.1", 11),
HTTP_2("HTTP/2.0", 20);
HTTP_2("HTTP/2.0", 20),
HTTP_3("HTTP/3.0", 30);
public static final Index<HttpVersion> CACHE = new Index.Builder<HttpVersion>()
.caseSensitive(false)

View File

@ -57,8 +57,8 @@ class EncodableEntry
{
byte staticBit = _referencedEntry.isStatic() ? (byte)0x40 : (byte)0x00;
buffer.put((byte)(0x80 | staticBit));
int relativeIndex = base - _referencedEntry.getIndex();
NBitInteger.encode(buffer, relativeIndex, 6);
int relativeIndex = _referencedEntry.getIndex() - base;
NBitInteger.encode(buffer, 6, relativeIndex);
}
else if (_referencedName != null)
{
@ -69,20 +69,20 @@ class EncodableEntry
// Encode the prefix.
buffer.put((byte)(0x40 | allowIntermediary | staticBit));
int relativeIndex = base - _referencedName.getIndex();
NBitInteger.encode(buffer, relativeIndex, 4);
int relativeIndex = _referencedName.getIndex() - base;
NBitInteger.encode(buffer, 4, relativeIndex);
// Encode the value.
if (huffman)
{
buffer.put((byte)0x80);
NBitInteger.encode(buffer, Huffman.octetsNeeded(value), 7);
NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(value));
Huffman.encode(buffer, value);
}
else
{
buffer.put((byte)0x00);
NBitInteger.encode(buffer, value.length(), 7);
NBitInteger.encode(buffer, 7, value.length());
buffer.put(value.getBytes());
}
}
@ -97,20 +97,20 @@ class EncodableEntry
if (huffman)
{
buffer.put((byte)(0x28 | allowIntermediary));
NBitInteger.encode(buffer, Huffman.octetsNeeded(name), 3);
NBitInteger.encode(buffer, 3, Huffman.octetsNeeded(name));
Huffman.encode(buffer, name);
buffer.put((byte)0x80);
NBitInteger.encode(buffer, Huffman.octetsNeeded(value), 7);
NBitInteger.encode(buffer, 7, Huffman.octetsNeeded(value));
Huffman.encode(buffer, value);
}
else
{
// TODO: What charset should we be using? (this applies to the instruction generators as well).
buffer.put((byte)(0x20 | allowIntermediary));
NBitInteger.encode(buffer, name.length(), 3);
NBitInteger.encode(buffer, 3, name.length());
buffer.put(name.getBytes());
buffer.put((byte)0x00);
NBitInteger.encode(buffer, value.length(), 7);
NBitInteger.encode(buffer, 7, value.length());
buffer.put(value.getBytes());
}
}
@ -118,10 +118,10 @@ class EncodableEntry
public int getRequiredInsertCount()
{
if (_referencedEntry != null)
return _referencedEntry.getIndex();
else if (_referencedName != null)
return _referencedName.getIndex();
if (_referencedEntry != null && !_referencedEntry.isStatic())
return _referencedEntry.getIndex() + 1;
else if (_referencedName != null && !_referencedName.isStatic())
return _referencedName.getIndex() + 1;
else
return 0;
}

View File

@ -18,8 +18,8 @@ import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http3.qpack.generator.DuplicateInstruction;
import org.eclipse.jetty.http3.qpack.generator.IndexedNameEntryInstruction;
import org.eclipse.jetty.http3.qpack.generator.InsertCountIncrementInstruction;
@ -53,12 +53,11 @@ public class QpackDecoder
private final NBitIntegerParser _integerDecoder = new NBitIntegerParser();
/**
* @param localMaxDynamicTableSize The maximum allowed size of the local dynamic header field table.
* @param maxHeaderSize The maximum allowed size of a headers block, expressed as total of all name and value characters, plus 32 per field
*/
public QpackDecoder(Handler handler, int localMaxDynamicTableSize, int maxHeaderSize)
public QpackDecoder(Handler handler, int maxHeaderSize)
{
_context = new QpackContext(localMaxDynamicTableSize);
_context = new QpackContext();
_builder = new MetaDataBuilder(maxHeaderSize);
_handler = handler;
}
@ -70,7 +69,8 @@ public class QpackDecoder
public interface Handler
{
void onMetadata(MetaData metaData);
// TODO: should this have the streamId?
void onHttpFields(HttpFields httpFields);
void onInstruction(Instruction instruction);
}
@ -84,10 +84,12 @@ public class QpackDecoder
if (buffer.remaining() > _builder.getMaxSize())
throw new QpackException.SessionException("431 Request Header Fields too large");
_integerDecoder.setPrefix(8);
int encodedInsertCount = _integerDecoder.decode(buffer);
if (encodedInsertCount < 0)
throw new QpackException.CompressionException("Could not parse Required Insert Count");
_integerDecoder.setPrefix(7);
boolean signBit = (buffer.get(buffer.position()) & 0x80) != 0;
int deltaBase = _integerDecoder.decode(buffer);
if (deltaBase < 0)
@ -104,10 +106,9 @@ public class QpackDecoder
EncodedFieldSection encodedFieldSection = new EncodedFieldSection(streamId, requiredInsertCount, base);
encodedFieldSection.parse(buffer);
if (encodedFieldSection.getRequiredInsertCount() >= insertCount)
if (encodedFieldSection.getRequiredInsertCount() <= insertCount)
{
MetaData metadata = encodedFieldSection.decode(_context, _builder);
_handler.onMetadata(metadata);
_handler.onHttpFields(encodedFieldSection.decode(_context));
_handler.onInstruction(new SectionAcknowledgmentInstruction(streamId));
}
else
@ -123,13 +124,52 @@ public class QpackDecoder
{
if (encodedFieldSection.getRequiredInsertCount() <= insertCount)
{
MetaData metadata = encodedFieldSection.decode(_context, _builder);
_handler.onMetadata(metadata);
_handler.onHttpFields(encodedFieldSection.decode(_context));
_handler.onInstruction(new SectionAcknowledgmentInstruction(encodedFieldSection.getStreamId()));
}
}
}
public void setCapacity(int capacity)
{
_context.getDynamicTable().setCapacity(capacity);
}
public void insert(int index) throws QpackException
{
DynamicTable dynamicTable = _context.getDynamicTable();
Entry entry = dynamicTable.get(index);
// Add the new Entry to the DynamicTable.
dynamicTable.add(entry);
_handler.onInstruction(new InsertCountIncrementInstruction(1));
checkEncodedFieldSections();
}
public void insert(int nameIndex, boolean isDynamicTableIndex, String value) throws QpackException
{
StaticTable staticTable = _context.getStaticTable();
DynamicTable dynamicTable = _context.getDynamicTable();
Entry referencedEntry = isDynamicTableIndex ? dynamicTable.get(nameIndex) : staticTable.get(nameIndex);
// Add the new Entry to the DynamicTable.
Entry entry = new Entry(new HttpField(referencedEntry.getHttpField().getHeader(), referencedEntry.getHttpField().getName(), value));
dynamicTable.add(entry);
_handler.onInstruction(new InsertCountIncrementInstruction(1));
checkEncodedFieldSections();
}
public void insert(String name, String value) throws QpackException
{
DynamicTable dynamicTable = _context.getDynamicTable();
Entry entry = new Entry(new HttpField(name, value));
// Add the new Entry to the DynamicTable.
dynamicTable.add(entry);
_handler.onInstruction(new InsertCountIncrementInstruction(1));
checkEncodedFieldSections();
}
public void onInstruction(Instruction instruction) throws QpackException
{
StaticTable staticTable = _context.getStaticTable();

View File

@ -27,13 +27,16 @@ import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.PreEncodedHttpField;
import org.eclipse.jetty.http3.qpack.generator.IndexedNameEntryInstruction;
import org.eclipse.jetty.http3.qpack.generator.Instruction;
import org.eclipse.jetty.http3.qpack.generator.LiteralNameEntryInstruction;
import org.eclipse.jetty.http3.qpack.generator.SetCapacityInstruction;
import org.eclipse.jetty.http3.qpack.table.DynamicTable;
import org.eclipse.jetty.http3.qpack.table.Entry;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.NullByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -96,38 +99,24 @@ public class QpackEncoder
void onInstruction(Instruction instruction);
}
private final ByteBufferPool _bufferPool;
private final Handler _handler;
private final QpackContext _context;
private final int _maxBlockedStreams;
private final Map<Integer, AtomicInteger> _blockedStreams = new HashMap<>();
private boolean _validateEncoding = true;
@Deprecated
public QpackEncoder()
public QpackEncoder(Handler handler, int maxBlockedStreams)
{
this(null, -1, -1);
this(handler, maxBlockedStreams, new NullByteBufferPool());
}
@Deprecated
public QpackEncoder(int localMaxDynamicTableSize)
{
this(null, -1, -1);
}
@Deprecated
public QpackEncoder(int localMaxDynamicTableSize, int remoteMaxDynamicTableSize)
{
this(null, -1, -1);
}
public QpackEncoder(Handler handler, int dynamicTableSize, int maxBlockedStreams)
public QpackEncoder(Handler handler, int maxBlockedStreams, ByteBufferPool bufferPool)
{
_handler = handler;
_bufferPool = bufferPool;
_context = new QpackContext();
_maxBlockedStreams = maxBlockedStreams;
// TODO: Fix.
_context.getDynamicTable().setCapacity(dynamicTableSize);
}
private boolean acquireBlockedStream(int streamId)
@ -156,6 +145,12 @@ public class QpackEncoder
_blockedStreams.remove(streamId);
}
public void setCapacity(int capacity)
{
_context.getDynamicTable().setCapacity(capacity);
_handler.onInstruction(new SetCapacityInstruction(capacity));
}
public QpackContext getQpackContext()
{
return _context;
@ -178,13 +173,11 @@ public class QpackEncoder
public static boolean shouldHuffmanEncode(HttpField httpField)
{
return !DO_NOT_HUFFMAN.contains(httpField.getHeader());
return false; //!DO_NOT_HUFFMAN.contains(httpField.getHeader());
}
public void encode(int streamId, ByteBuffer buffer, MetaData metadata) throws QpackException
public ByteBuffer encode(int streamId, HttpFields httpFields) throws QpackException
{
HttpFields httpFields = metadata.getFields();
// Verify that we can encode without errors.
if (isValidateEncoding() && httpFields != null)
{
@ -192,7 +185,7 @@ public class QpackEncoder
{
String name = field.getName();
char firstChar = name.charAt(0);
if (firstChar <= ' ' || firstChar == ':')
if (firstChar <= ' ')
throw new QpackException.StreamException("Invalid header name: '%s'", name);
}
}
@ -215,21 +208,27 @@ public class QpackEncoder
DynamicTable dynamicTable = _context.getDynamicTable();
int base = dynamicTable.getBase();
int encodedInsertCount = encodeInsertCount(requiredInsertCount, dynamicTable.getInsertCount());
int encodedInsertCount = encodeInsertCount(requiredInsertCount, dynamicTable.getCapacity());
boolean signBit = base < requiredInsertCount;
int deltaBase = signBit ? requiredInsertCount - base - 1 : base - requiredInsertCount;
// TODO: Calculate the size required.
ByteBuffer buffer = _bufferPool.acquire(1024, false);
int pos = BufferUtil.flipToFill(buffer);
// Encode the Field Section Prefix into the ByteBuffer.
buffer.put((byte)0x00);
NBitInteger.encode(buffer, encodedInsertCount, 8);
buffer.put(signBit ? (byte)0x01 : (byte)0x00);
NBitInteger.encode(buffer, deltaBase, 7);
NBitInteger.encode(buffer, 8, encodedInsertCount);
buffer.put(signBit ? (byte)0x80 : (byte)0x00);
NBitInteger.encode(buffer, 7, deltaBase);
// Encode the field lines into the ByteBuffer.
for (EncodableEntry entry : encodableEntries)
{
entry.encode(buffer, base);
}
BufferUtil.flipToFlush(buffer, pos);
return buffer;
}
private EncodableEntry encode(int streamId, HttpField field)

View File

@ -18,8 +18,7 @@ import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http3.qpack.MetaDataBuilder;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http3.qpack.QpackContext;
import org.eclipse.jetty.http3.qpack.QpackException;
@ -71,17 +70,17 @@ public class EncodedFieldSection
}
}
public MetaData decode(QpackContext context, MetaDataBuilder builder) throws QpackException
public HttpFields decode(QpackContext context) throws QpackException
{
if (context.getDynamicTable().getInsertCount() < _requiredInsertCount)
throw new IllegalStateException("Required Insert Count Not Reached");
HttpFields.Mutable httpFields = HttpFields.build();
for (EncodedField encodedField : _encodedFields)
{
builder.emit(encodedField.decode(context));
httpFields.add(encodedField.decode(context));
}
return builder.build();
return httpFields;
}
private EncodedField parseIndexedFieldLine(ByteBuffer buffer) throws QpackException
@ -200,7 +199,7 @@ public class EncodedFieldSection
public HttpField decode(QpackContext context) throws QpackException
{
if (_dynamicTable)
return context.getDynamicTable().getAbsolute(_base + _index + 1).getHttpField();
return context.getDynamicTable().getAbsolute(_base + _index).getHttpField();
else
return context.getStaticTable().get(_index).getHttpField();
}

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.http3.qpack.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http3.qpack.QpackDecoder;
import org.eclipse.jetty.http3.qpack.QpackException;
/**
@ -51,15 +52,55 @@ public class EncoderInstructionParser
public interface Handler
{
void onSetDynamicTableCapacity(int capacity);
void onSetDynamicTableCapacity(int capacity) throws QpackException;
void onDuplicate(int index);
void onDuplicate(int index) throws QpackException;
void onInsertNameWithReference(int nameIndex, boolean isDynamicTableIndex, String value);
void onInsertNameWithReference(int nameIndex, boolean isDynamicTableIndex, String value) throws QpackException;
void onInsertWithLiteralName(String name, String value);
void onInsertWithLiteralName(String name, String value) throws QpackException;
}
public static class DecoderHandler implements Handler
{
private final QpackDecoder _decoder;
public DecoderHandler(QpackDecoder decoder)
{
_decoder = decoder;
}
@Override
public void onSetDynamicTableCapacity(int capacity)
{
_decoder.setCapacity(capacity);
}
@Override
public void onDuplicate(int index) throws QpackException
{
_decoder.insert(index);
}
@Override
public void onInsertNameWithReference(int nameIndex, boolean isDynamicTableIndex, String value) throws QpackException
{
_decoder.insert(nameIndex, isDynamicTableIndex, value);
}
@Override
public void onInsertWithLiteralName(String name, String value) throws QpackException
{
_decoder.insert(name, value);
}
}
public EncoderInstructionParser(QpackDecoder decoder)
{
this(new DecoderHandler(decoder));
}
public EncoderInstructionParser(Handler handler)
{
_handler = handler;
@ -196,7 +237,7 @@ public class EncoderInstructionParser
}
}
private void parseDuplicate(ByteBuffer buffer)
private void parseDuplicate(ByteBuffer buffer) throws QpackException
{
int index = _integerParser.decode(buffer);
if (index >= 0)
@ -206,7 +247,7 @@ public class EncoderInstructionParser
}
}
private void parseSetDynamicTableCapacity(ByteBuffer buffer)
private void parseSetDynamicTableCapacity(ByteBuffer buffer) throws QpackException
{
int capacity = _integerParser.decode(buffer);
if (capacity >= 0)

View File

@ -24,6 +24,8 @@ public class NBitIntegerParser
public void setPrefix(int prefix)
{
if (_started)
throw new IllegalStateException();
_prefix = prefix;
}

View File

@ -46,6 +46,8 @@ public class NBitStringParser
public void setPrefix(int prefix)
{
if (_state != State.PARSING)
throw new IllegalStateException();
_prefix = prefix;
}

View File

@ -57,7 +57,7 @@ public class DynamicTable
// Set the Entries absolute index which will never change.
entry.setIndex(_absoluteIndex++);
_entries.add(0, entry);
_entries.add(entry);
_fieldMap.put(entry.getHttpField(), entry);
_nameMap.put(entry.getHttpField().getLowerCaseName(), entry);

View File

@ -29,173 +29,6 @@ public class StaticTable
{
private static final String EMPTY = "";
public static final String[][] STATIC_TABLE =
{
{null, null},
/* 1 */ {":authority", EMPTY},
/* 2 */ {":method", "GET"},
/* 3 */ {":method", "POST"},
/* 4 */ {":path", "/"},
/* 5 */ {":path", "/index.html"},
/* 6 */ {":scheme", "http"},
/* 7 */ {":scheme", "https"},
/* 8 */ {":status", "200"},
/* 9 */ {":status", "204"},
/* 10 */ {":status", "206"},
/* 11 */ {":status", "304"},
/* 12 */ {":status", "400"},
/* 13 */ {":status", "404"},
/* 14 */ {":status", "500"},
/* 15 */ {"accept-charset", EMPTY},
/* 16 */ {"accept-encoding", "gzip, deflate"},
/* 17 */ {"accept-language", EMPTY},
/* 18 */ {"accept-ranges", EMPTY},
/* 19 */ {"accept", EMPTY},
/* 20 */ {"access-control-allow-origin", EMPTY},
/* 21 */ {"age", EMPTY},
/* 22 */ {"allow", EMPTY},
/* 23 */ {"authorization", EMPTY},
/* 24 */ {"cache-control", EMPTY},
/* 25 */ {"content-disposition", EMPTY},
/* 26 */ {"content-encoding", EMPTY},
/* 27 */ {"content-language", EMPTY},
/* 28 */ {"content-length", EMPTY},
/* 29 */ {"content-location", EMPTY},
/* 30 */ {"content-range", EMPTY},
/* 31 */ {"content-type", EMPTY},
/* 32 */ {"cookie", EMPTY},
/* 33 */ {"date", EMPTY},
/* 34 */ {"etag", EMPTY},
/* 35 */ {"expect", EMPTY},
/* 36 */ {"expires", EMPTY},
/* 37 */ {"from", EMPTY},
/* 38 */ {"host", EMPTY},
/* 39 */ {"if-match", EMPTY},
/* 40 */ {"if-modified-since", EMPTY},
/* 41 */ {"if-none-match", EMPTY},
/* 42 */ {"if-range", EMPTY},
/* 43 */ {"if-unmodified-since", EMPTY},
/* 44 */ {"last-modified", EMPTY},
/* 45 */ {"link", EMPTY},
/* 46 */ {"location", EMPTY},
/* 47 */ {"max-forwards", EMPTY},
/* 48 */ {"proxy-authenticate", EMPTY},
/* 49 */ {"proxy-authorization", EMPTY},
/* 50 */ {"range", EMPTY},
/* 51 */ {"referer", EMPTY},
/* 52 */ {"refresh", EMPTY},
/* 53 */ {"retry-after", EMPTY},
/* 54 */ {"server", EMPTY},
/* 55 */ {"set-cookie", EMPTY},
/* 56 */ {"strict-transport-security", EMPTY},
/* 57 */ {"transfer-encoding", EMPTY},
/* 58 */ {"user-agent", EMPTY},
/* 59 */ {"vary", EMPTY},
/* 60 */ {"via", EMPTY},
/* 61 */ {"www-authenticate", EMPTY}
};
public static final int STATIC_SIZE = STATIC_TABLE.length - 1;
private final Map<HttpField, Entry> _staticFieldMap = new HashMap<>();
private final Index<StaticEntry> _staticNameMap;
private final StaticEntry[] _staticTableByHeader = new StaticEntry[HttpHeader.values().length];
private final StaticEntry[] _staticTable = new StaticEntry[STATIC_TABLE.length];
public StaticTable()
{
Index.Builder<StaticEntry> staticNameMapBuilder = new Index.Builder<StaticEntry>().caseSensitive(false);
Set<String> added = new HashSet<>();
for (int i = 1; i < STATIC_TABLE.length; i++)
{
StaticEntry entry = null;
String name = STATIC_TABLE[i][0];
String value = STATIC_TABLE[i][1];
HttpHeader header = HttpHeader.CACHE.get(name);
if (header != null && value != null)
{
switch (header)
{
case C_METHOD:
{
HttpMethod method = HttpMethod.CACHE.get(value);
if (method != null)
entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, method));
break;
}
case C_SCHEME:
{
HttpScheme scheme = HttpScheme.CACHE.get(value);
if (scheme != null)
entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, scheme));
break;
}
case C_STATUS:
{
entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, value));
break;
}
default:
break;
}
}
if (entry == null)
entry = new StaticEntry(i, header == null ? new HttpField(STATIC_TABLE[i][0], value) : new HttpField(header, name, value));
_staticTable[i] = entry;
if (entry.getHttpField().getValue() != null)
_staticFieldMap.put(entry.getHttpField(), entry);
if (!added.contains(entry.getHttpField().getName()))
{
added.add(entry.getHttpField().getName());
staticNameMapBuilder.with(entry.getHttpField().getName(), entry);
}
}
_staticNameMap = staticNameMapBuilder.build();
for (HttpHeader h : HttpHeader.values())
{
StaticEntry entry = _staticNameMap.get(h.asString());
if (entry != null)
_staticTableByHeader[h.ordinal()] = entry;
}
}
public Entry get(HttpField field)
{
return _staticFieldMap.get(field);
}
public Entry get(String name)
{
return _staticNameMap.get(name);
}
public Entry get(int index)
{
if (index >= _staticTable.length)
return null;
return _staticTable[index];
}
public Entry get(HttpHeader header)
{
int index = header.ordinal();
if (index >= _staticTableByHeader.length)
return null;
return _staticTableByHeader[index];
}
@SuppressWarnings("unused")
public static final String[][] QPACK_STATIC_TABLE =
{
{":authority", ""},
{":path", "/"},
@ -297,4 +130,104 @@ public class StaticTable
{"x-frame-options", "deny"},
{"x-frame-options", "sameorigin"},
};
public static final int STATIC_SIZE = STATIC_TABLE.length - 1;
private final Map<HttpField, Entry> _staticFieldMap = new HashMap<>();
private final Index<StaticEntry> _staticNameMap;
private final StaticEntry[] _staticTableByHeader = new StaticEntry[HttpHeader.values().length];
private final StaticEntry[] _staticTable = new StaticEntry[STATIC_TABLE.length];
public StaticTable()
{
Index.Builder<StaticEntry> staticNameMapBuilder = new Index.Builder<StaticEntry>().caseSensitive(false);
Set<String> added = new HashSet<>();
for (int i = 0; i < STATIC_TABLE.length; i++)
{
StaticEntry entry = null;
String name = STATIC_TABLE[i][0];
String value = STATIC_TABLE[i][1];
HttpHeader header = HttpHeader.CACHE.get(name);
if (header != null && value != null)
{
switch (header)
{
case C_METHOD:
{
HttpMethod method = HttpMethod.CACHE.get(value);
if (method != null)
entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, method));
break;
}
case C_SCHEME:
{
HttpScheme scheme = HttpScheme.CACHE.get(value);
if (scheme != null)
entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, scheme));
break;
}
case C_STATUS:
{
entry = new StaticEntry(i, new StaticTableHttpField(header, name, value, value));
break;
}
default:
break;
}
}
if (entry == null)
entry = new StaticEntry(i, header == null ? new HttpField(STATIC_TABLE[i][0], value) : new HttpField(header, name, value));
_staticTable[i] = entry;
if (entry.getHttpField().getValue() != null)
_staticFieldMap.put(entry.getHttpField(), entry);
if (!added.contains(entry.getHttpField().getName()))
{
added.add(entry.getHttpField().getName());
staticNameMapBuilder.with(entry.getHttpField().getName(), entry);
}
}
_staticNameMap = staticNameMapBuilder.build();
for (HttpHeader h : HttpHeader.values())
{
StaticEntry entry = _staticNameMap.get(h.asString());
if (entry != null)
_staticTableByHeader[h.ordinal()] = entry;
}
}
public Entry get(HttpField field)
{
return _staticFieldMap.get(field);
}
public Entry get(String name)
{
return _staticNameMap.get(name);
}
public Entry get(int index)
{
if (index >= _staticTable.length)
return null;
return _staticTable[index];
}
public Entry get(HttpHeader header)
{
int index = header.ordinal();
if (index >= _staticTableByHeader.length)
return null;
return _staticTableByHeader[index];
}
}

View File

@ -0,0 +1,147 @@
//
// ========================================================================
// 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.http.HttpFields;
import org.eclipse.jetty.http3.qpack.generator.IndexedNameEntryInstruction;
import org.eclipse.jetty.http3.qpack.generator.InsertCountIncrementInstruction;
import org.eclipse.jetty.http3.qpack.generator.Instruction;
import org.eclipse.jetty.http3.qpack.generator.SectionAcknowledgmentInstruction;
import org.eclipse.jetty.http3.qpack.generator.SetCapacityInstruction;
import org.eclipse.jetty.http3.qpack.parser.DecoderInstructionParser;
import org.eclipse.jetty.http3.qpack.parser.EncoderInstructionParser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.NullByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class EncodeDecodeTest
{
private QpackEncoder _encoder;
private QpackDecoder _decoder;
private TestDecoderHandler _decoderHandler;
private TestEncoderHandler _encoderHandler;
private DecoderInstructionParser _decoderInstructionParser;
private EncoderInstructionParser _encoderInstructionParser;
private final int MAX_BLOCKED_STREAMS = 5;
private final int MAX_HEADER_SIZE = 1024;
@BeforeEach
public void before()
{
_encoderHandler = new TestEncoderHandler();
_decoderHandler = new TestDecoderHandler();
_encoder = new QpackEncoder(_encoderHandler, MAX_BLOCKED_STREAMS);
_decoder = new QpackDecoder(_decoderHandler, MAX_HEADER_SIZE);
_encoderInstructionParser = new EncoderInstructionParser(_decoder);
}
@Test
public void test() throws Exception
{
// B.1. Literal Field Line With Name Reference.
int streamId = 0;
HttpFields httpFields = HttpFields.build().add(":path", "/index.html");
ByteBuffer buffer = _encoder.encode(streamId, httpFields);
assertNull(_encoderHandler.getInstruction());
assertThat(BufferUtil.toHexString(buffer), equalsHex("0000 510b 2f69 6e64 6578 2e68 746d 6c"));
assertTrue(_encoderHandler.isEmpty());
_decoder.decode(streamId, buffer);
HttpFields result = _decoderHandler.getHttpFields();
assertThat(result, is(httpFields));
assertThat(_decoderHandler.getInstruction(), instanceOf(SectionAcknowledgmentInstruction.class));
assertTrue(_decoderHandler.isEmpty());
// B.2. Dynamic Table.
// Set capacity to 220.
_encoder.setCapacity(220);
Instruction instruction = _encoderHandler.getInstruction();
assertThat(instruction, instanceOf(SetCapacityInstruction.class));
assertThat(((SetCapacityInstruction)instruction).getCapacity(), is(220));
assertThat(BufferUtil.toHexString(toBuffer(instruction)), equalsHex("3fbd01"));
_encoderInstructionParser.parse(toHex("3fbd01"));
assertThat(_decoder.getQpackContext().getDynamicTable().getCapacity(), is(220));
// Insert with named referenced to static table. Test we get two instructions generated to add to the dynamic table.
httpFields = HttpFields.build()
.add(":authority", "www.example.com")
.add(":path", "/sample/path");
buffer = _encoder.encode(4, httpFields);
instruction = _encoderHandler.getInstruction();
assertThat(instruction, instanceOf(IndexedNameEntryInstruction.class));
assertThat(((IndexedNameEntryInstruction)instruction).getIndex(), is(0));
assertThat(((IndexedNameEntryInstruction)instruction).getValue(), is("www.example.com"));
assertThat(BufferUtil.toHexString(toBuffer(instruction)), equalsHex("c00f 7777 772e 6578 616d 706c 652e 636f 6d"));
instruction = _encoderHandler.getInstruction();
assertThat(instruction, instanceOf(IndexedNameEntryInstruction.class));
assertThat(((IndexedNameEntryInstruction)instruction).getIndex(), is(1));
assertThat(((IndexedNameEntryInstruction)instruction).getValue(), is("/sample/path"));
assertThat(BufferUtil.toHexString(toBuffer(instruction)), equalsHex("c10c 2f73 616d 706c 652f 7061 7468"));
assertTrue(_encoderHandler.isEmpty());
// We cannot decode the buffer until we parse the two instructions generated above (we reach required insert count).
_decoder.decode(4, buffer);
assertNull(_decoderHandler.getHttpFields());
_encoderInstructionParser.parse(toHex("c00f 7777 772e 6578 616d 706c 652e 636f 6d"));
assertNull(_decoderHandler.getHttpFields());
assertThat(_decoderHandler.getInstruction(), instanceOf(InsertCountIncrementInstruction.class));
_encoderInstructionParser.parse(toHex("c10c 2f73 616d 706c 652f 7061 7468"));
assertThat(_decoderHandler.getHttpFields(), is(httpFields));
assertThat(_decoderHandler.getInstruction(), instanceOf(InsertCountIncrementInstruction.class));
assertThat(_decoderHandler.getInstruction(), instanceOf(SectionAcknowledgmentInstruction.class));
assertTrue(_decoderHandler.isEmpty());
}
public static ByteBuffer toBuffer(Instruction instruction)
{
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(new NullByteBufferPool());
instruction.encode(lease);
assertThat(lease.getSize(), is(1));
return lease.getByteBuffers().get(0);
}
public static ByteBuffer toHex(String hexString)
{
hexString = hexString.replaceAll("\\s+", "");
return ByteBuffer.wrap(TypeUtil.fromHexString(hexString));
}
public static Matcher<java.lang.String> equalsHex(String expectedString)
{
expectedString = expectedString.replaceAll("\\s+", "");
return org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase(expectedString);
}
}

View File

@ -16,18 +16,18 @@ package org.eclipse.jetty.http3.qpack;
import java.util.LinkedList;
import java.util.Queue;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http3.qpack.generator.Instruction;
public class DecoderTestHandler implements QpackDecoder.Handler
public class TestDecoderHandler implements QpackDecoder.Handler
{
private final Queue<MetaData> _metadataList = new LinkedList<>();
private final Queue<HttpFields> _httpFieldsList = new LinkedList<>();
private final Queue<Instruction> _instructionList = new LinkedList<>();
@Override
public void onMetadata(MetaData metaData)
public void onHttpFields(HttpFields httpFields)
{
_metadataList.add(metaData);
_httpFieldsList.add(httpFields);
}
@Override
@ -36,9 +36,9 @@ public class DecoderTestHandler implements QpackDecoder.Handler
_instructionList.add(instruction);
}
public MetaData getMetaData()
public HttpFields getHttpFields()
{
return _metadataList.poll();
return _httpFieldsList.poll();
}
public Instruction getInstruction()
@ -48,6 +48,6 @@ public class DecoderTestHandler implements QpackDecoder.Handler
public boolean isEmpty()
{
return _metadataList.isEmpty() && _instructionList.isEmpty();
return _httpFieldsList.isEmpty() && _instructionList.isEmpty();
}
}

View File

@ -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.util.LinkedList;
import java.util.Queue;
import org.eclipse.jetty.http3.qpack.generator.Instruction;
public class TestEncoderHandler implements QpackEncoder.Handler
{
private final Queue<Instruction> _instructionList = new LinkedList<>();
@Override
public void onInstruction(Instruction instruction)
{
_instructionList.add(instruction);
}
public Instruction getInstruction()
{
return _instructionList.poll();
}
public boolean isEmpty()
{
return _instructionList.isEmpty();
}
}