Qpack refactoring, testing and bug fixes.
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
a4938a3f4a
commit
191b1af880
|
@ -61,18 +61,53 @@ abstract class EncodableEntry
|
|||
@Override
|
||||
public void encode(ByteBuffer buffer, int base)
|
||||
{
|
||||
byte staticBit = _entry.isStatic() ? (byte)0x40 : (byte)0x00;
|
||||
buffer.put((byte)(0x80 | staticBit));
|
||||
int relativeIndex = _entry.getIndex() - base;
|
||||
boolean isStatic = _entry.isStatic();
|
||||
if (isStatic)
|
||||
{
|
||||
// Indexed Field Line with Static Reference.
|
||||
buffer.put((byte)(0x80 | 0x40));
|
||||
int relativeIndex = _entry.getIndex();
|
||||
NBitInteger.encode(buffer, 6, relativeIndex);
|
||||
}
|
||||
else if (_entry.getIndex() < base)
|
||||
{
|
||||
// Indexed Field Line with Dynamic Reference.
|
||||
buffer.put((byte)0x80);
|
||||
int relativeIndex = base - (_entry.getIndex() + 1);
|
||||
NBitInteger.encode(buffer, 6, relativeIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Indexed Field Line with Post-Base Index.
|
||||
buffer.put((byte)0x10);
|
||||
int relativeIndex = _entry.getIndex() - base;
|
||||
NBitInteger.encode(buffer, 4, relativeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequiredSize(int base)
|
||||
{
|
||||
int relativeIndex = _entry.getIndex() - base;
|
||||
boolean isStatic = _entry.isStatic();
|
||||
if (isStatic)
|
||||
{
|
||||
// Indexed Field Line with Static Reference.
|
||||
int relativeIndex = _entry.getIndex();
|
||||
return 1 + NBitInteger.octectsNeeded(6, relativeIndex);
|
||||
}
|
||||
else if (_entry.getIndex() < base)
|
||||
{
|
||||
// Indexed Field Line with Dynamic Reference.
|
||||
int relativeIndex = base - (_entry.getIndex() + 1);
|
||||
return 1 + NBitInteger.octectsNeeded(6, relativeIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Indexed Field Line with Post-Base Index.
|
||||
int relativeIndex = _entry.getIndex() - base;
|
||||
return 1 + NBitInteger.octectsNeeded(4, relativeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRequiredInsertCount()
|
||||
|
@ -97,13 +132,33 @@ abstract class EncodableEntry
|
|||
@Override
|
||||
public void encode(ByteBuffer buffer, int base)
|
||||
{
|
||||
byte allowIntermediary = 0x00; // TODO: this is 0x20 bit, when should this be set?
|
||||
byte staticBit = _nameEntry.isStatic() ? (byte)0x10 : (byte)0x00;
|
||||
// TODO: when should this be set?
|
||||
// TODO: this is different for the Post-Base index case.
|
||||
byte allowIntermediary = 0x00;
|
||||
|
||||
// Encode the prefix.
|
||||
buffer.put((byte)(0x40 | allowIntermediary | staticBit));
|
||||
int relativeIndex = _nameEntry.getIndex() - base;
|
||||
boolean isStatic = _nameEntry.isStatic();
|
||||
if (isStatic)
|
||||
{
|
||||
// Literal Field Line with Static Name Reference.
|
||||
buffer.put((byte)(0x40 | allowIntermediary | 0x10));
|
||||
int relativeIndex = _nameEntry.getIndex();
|
||||
NBitInteger.encode(buffer, 4, relativeIndex);
|
||||
}
|
||||
else if (_nameEntry.getIndex() < base)
|
||||
{
|
||||
// Literal Field Line with Dynamic Name Reference.
|
||||
buffer.put((byte)(0x40 | allowIntermediary));
|
||||
int relativeIndex = base - (_nameEntry.getIndex() + 1);
|
||||
NBitInteger.encode(buffer, 4, relativeIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Literal Field Line with Post-Base Name Reference.
|
||||
buffer.put(allowIntermediary);
|
||||
int relativeIndex = _nameEntry.getIndex() - base;
|
||||
NBitInteger.encode(buffer, 3, relativeIndex);
|
||||
}
|
||||
|
||||
// Encode the value.
|
||||
String value = getValue();
|
||||
|
|
|
@ -75,4 +75,17 @@ public class QpackContext
|
|||
|
||||
return _dynamicTable.get(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative Index of an entry.
|
||||
* @param entry the entry to get the index of.
|
||||
* @return the relative index of the entry.
|
||||
*/
|
||||
public int indexOf(Entry entry)
|
||||
{
|
||||
if (entry.isStatic())
|
||||
return entry.getIndex();
|
||||
|
||||
return _dynamicTable.index(entry);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
package org.eclipse.jetty.http3.qpack;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -23,11 +24,13 @@ import org.eclipse.jetty.http.HttpHeader;
|
|||
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.parser.DecoderInstructionParser;
|
||||
import org.eclipse.jetty.http3.qpack.parser.EncodedFieldSection;
|
||||
import org.eclipse.jetty.http3.qpack.parser.NBitIntegerParser;
|
||||
import org.eclipse.jetty.http3.qpack.table.DynamicTable;
|
||||
import org.eclipse.jetty.http3.qpack.table.Entry;
|
||||
import org.eclipse.jetty.http3.qpack.table.StaticTable;
|
||||
import org.eclipse.jetty.util.component.Dumpable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -35,7 +38,7 @@ import org.slf4j.LoggerFactory;
|
|||
* Qpack Decoder
|
||||
* <p>This is not thread safe and may only be called by 1 thread at a time.</p>
|
||||
*/
|
||||
public class QpackDecoder
|
||||
public class QpackDecoder implements Dumpable
|
||||
{
|
||||
public static final Logger LOG = LoggerFactory.getLogger(QpackDecoder.class);
|
||||
public static final HttpField.LongValueHttpField CONTENT_LENGTH_0 =
|
||||
|
@ -44,6 +47,7 @@ public class QpackDecoder
|
|||
private final Handler _handler;
|
||||
private final QpackContext _context;
|
||||
private final MetaDataBuilder _builder;
|
||||
private final DecoderInstructionParser _parser;
|
||||
|
||||
private final List<EncodedFieldSection> _encodedFieldSections = new ArrayList<>();
|
||||
private final NBitIntegerParser _integerDecoder = new NBitIntegerParser();
|
||||
|
@ -56,6 +60,7 @@ public class QpackDecoder
|
|||
_context = new QpackContext();
|
||||
_builder = new MetaDataBuilder(maxHeaderSize);
|
||||
_handler = handler;
|
||||
_parser = new DecoderInstructionParser(new DecoderAdapter());
|
||||
}
|
||||
|
||||
public QpackContext getQpackContext()
|
||||
|
@ -67,7 +72,7 @@ public class QpackDecoder
|
|||
{
|
||||
void onHttpFields(int streamId, HttpFields httpFields);
|
||||
|
||||
void onInstruction(Instruction instruction);
|
||||
void onInstruction(Instruction instruction) throws QpackException;
|
||||
}
|
||||
|
||||
public void decode(int streamId, ByteBuffer buffer) throws QpackException
|
||||
|
@ -103,7 +108,7 @@ public class QpackDecoder
|
|||
EncodedFieldSection encodedFieldSection = new EncodedFieldSection(streamId, requiredInsertCount, base);
|
||||
encodedFieldSection.parse(buffer);
|
||||
|
||||
if (encodedFieldSection.getRequiredInsertCount() <= insertCount)
|
||||
if (requiredInsertCount <= insertCount)
|
||||
{
|
||||
_handler.onHttpFields(streamId, encodedFieldSection.decode(_context));
|
||||
_handler.onInstruction(new SectionAcknowledgmentInstruction(streamId));
|
||||
|
@ -115,6 +120,11 @@ public class QpackDecoder
|
|||
}
|
||||
}
|
||||
|
||||
public void parseInstruction(ByteBuffer buffer) throws QpackException
|
||||
{
|
||||
_parser.parse(buffer);
|
||||
}
|
||||
|
||||
private void checkEncodedFieldSections() throws QpackException
|
||||
{
|
||||
int insertCount = _context.getDynamicTable().getInsertCount();
|
||||
|
@ -128,7 +138,7 @@ public class QpackDecoder
|
|||
}
|
||||
}
|
||||
|
||||
public void setCapacity(int capacity)
|
||||
void setCapacity(int capacity)
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
|
@ -136,7 +146,7 @@ public class QpackDecoder
|
|||
}
|
||||
}
|
||||
|
||||
public void insert(int index) throws QpackException
|
||||
void insert(int index) throws QpackException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
|
@ -150,11 +160,11 @@ public class QpackDecoder
|
|||
}
|
||||
}
|
||||
|
||||
public void insert(int nameIndex, boolean isDynamicTableIndex, String value) throws QpackException
|
||||
void insert(int nameIndex, boolean isDynamicTableIndex, String value) throws QpackException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
StaticTable staticTable = _context.getStaticTable();
|
||||
StaticTable staticTable = QpackContext.getStaticTable();
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
Entry referencedEntry = isDynamicTableIndex ? dynamicTable.get(nameIndex) : staticTable.get(nameIndex);
|
||||
|
||||
|
@ -166,14 +176,14 @@ public class QpackDecoder
|
|||
}
|
||||
}
|
||||
|
||||
public void insert(String name, String value) throws QpackException
|
||||
void insert(String name, String value) throws QpackException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
Entry entry = new Entry(new HttpField(name, value));
|
||||
|
||||
// Add the new Entry to the DynamicTable.
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
dynamicTable.add(entry);
|
||||
_handler.onInstruction(new InsertCountIncrementInstruction(1));
|
||||
checkEncodedFieldSections();
|
||||
|
@ -210,9 +220,45 @@ public class QpackDecoder
|
|||
return reqInsertCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Appendable out, String indent) throws IOException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
Dumpable.dumpObjects(out, indent, _context.getDynamicTable());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("QpackDecoder@%x{%s}", hashCode(), _context);
|
||||
}
|
||||
|
||||
class DecoderAdapter implements DecoderInstructionParser.Handler
|
||||
{
|
||||
@Override
|
||||
public void onSetDynamicTableCapacity(int capacity)
|
||||
{
|
||||
setCapacity(capacity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDuplicate(int index) throws QpackException
|
||||
{
|
||||
insert(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInsertNameWithReference(int nameIndex, boolean isDynamicTableIndex, String value) throws QpackException
|
||||
{
|
||||
insert(nameIndex, isDynamicTableIndex, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInsertWithLiteralName(String name, String value) throws QpackException
|
||||
{
|
||||
insert(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
package org.eclipse.jetty.http3.qpack;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
|
@ -32,15 +33,17 @@ 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.parser.EncoderInstructionParser;
|
||||
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.eclipse.jetty.util.component.Dumpable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class QpackEncoder
|
||||
public class QpackEncoder implements Dumpable
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QpackEncoder.class);
|
||||
private static final HttpField[] STATUSES = new HttpField[599];
|
||||
|
@ -96,17 +99,17 @@ public class QpackEncoder
|
|||
|
||||
public interface Handler
|
||||
{
|
||||
void onInstruction(Instruction instruction);
|
||||
void onInstruction(Instruction instruction) throws QpackException;
|
||||
}
|
||||
|
||||
private final ByteBufferPool _bufferPool;
|
||||
private final Handler _handler;
|
||||
private final QpackContext _context;
|
||||
private final int _maxBlockedStreams;
|
||||
private int _knownInsertCount;
|
||||
|
||||
private int _blockedStreams = 0;
|
||||
private final Map<Integer, StreamInfo> _streamInfoMap = new HashMap<>();
|
||||
private final EncoderInstructionParser _parser;
|
||||
private int _knownInsertCount;
|
||||
private int _blockedStreams = 0;
|
||||
|
||||
public QpackEncoder(Handler handler, int maxBlockedStreams)
|
||||
{
|
||||
|
@ -120,6 +123,7 @@ public class QpackEncoder
|
|||
_context = new QpackContext();
|
||||
_maxBlockedStreams = maxBlockedStreams;
|
||||
_knownInsertCount = 0;
|
||||
_parser = new EncoderInstructionParser(new EncoderAdapter());
|
||||
}
|
||||
|
||||
public QpackContext getQpackContext()
|
||||
|
@ -127,7 +131,12 @@ public class QpackEncoder
|
|||
return _context;
|
||||
}
|
||||
|
||||
public void setCapacity(int capacity)
|
||||
/**
|
||||
* Set the capacity of the DynamicTable and send a instruction to set the capacity on the remote Decoder.
|
||||
* @param capacity the new capacity.
|
||||
* @throws QpackException
|
||||
*/
|
||||
public void setCapacity(int capacity) throws QpackException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
|
@ -136,26 +145,23 @@ public class QpackEncoder
|
|||
}
|
||||
}
|
||||
|
||||
public void insertCountIncrement(int increment) throws QpackException
|
||||
public void parseInstruction(ByteBuffer buffer) throws QpackException
|
||||
{
|
||||
_parser.parse(buffer);
|
||||
}
|
||||
|
||||
void insertCountIncrement(int increment) throws QpackException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
int insertCount = _context.getDynamicTable().getInsertCount();
|
||||
if (_knownInsertCount + increment > insertCount)
|
||||
throw new QpackException.StreamException("KnownInsertCount incremented over InsertCount");
|
||||
|
||||
// TODO: release any references to entries which used to insert new entries.
|
||||
for (Entry entry : _context.getDynamicTable())
|
||||
{
|
||||
if (entry.getIndex() > _knownInsertCount)
|
||||
break;
|
||||
}
|
||||
|
||||
_knownInsertCount += increment;
|
||||
}
|
||||
}
|
||||
|
||||
public void sectionAcknowledgement(int streamId) throws QpackException
|
||||
void sectionAcknowledgement(int streamId) throws QpackException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
|
@ -165,6 +171,7 @@ public class QpackEncoder
|
|||
|
||||
// The KnownInsertCount should be updated to the earliest sent RequiredInsertCount on that stream.
|
||||
StreamInfo.SectionInfo sectionInfo = streamInfo.acknowledge();
|
||||
sectionInfo.release();
|
||||
_knownInsertCount = Math.max(_knownInsertCount, sectionInfo.getRequiredInsertCount());
|
||||
|
||||
// If we have no more outstanding section acknowledgments remove the StreamInfo.
|
||||
|
@ -173,17 +180,23 @@ public class QpackEncoder
|
|||
}
|
||||
}
|
||||
|
||||
public void streamCancellation(int streamId) throws QpackException
|
||||
void streamCancellation(int streamId) throws QpackException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
StreamInfo streamInfo = _streamInfoMap.remove(streamId);
|
||||
if (streamInfo == null)
|
||||
throw new QpackException.StreamException("No StreamInfo for " + streamId);
|
||||
|
||||
// Release all referenced entries outstanding on the stream that was cancelled.
|
||||
for (StreamInfo.SectionInfo sectionInfo : streamInfo)
|
||||
{
|
||||
sectionInfo.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean referenceEntry(Entry entry)
|
||||
private boolean referenceEntry(Entry entry, StreamInfo streamInfo)
|
||||
{
|
||||
if (entry == null)
|
||||
return false;
|
||||
|
@ -195,32 +208,28 @@ public class QpackEncoder
|
|||
if (inEvictionZone)
|
||||
return false;
|
||||
|
||||
StreamInfo.SectionInfo sectionInfo = streamInfo.getCurrentSectionInfo();
|
||||
|
||||
// If they have already acknowledged this entry we can reference it straight away.
|
||||
if (_knownInsertCount >= entry.getIndex() + 1)
|
||||
{
|
||||
entry.reference();
|
||||
sectionInfo.reference(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean referenceEntry(Entry entry, StreamInfo streamInfo)
|
||||
{
|
||||
if (referenceEntry(entry))
|
||||
return true;
|
||||
|
||||
// We may need to risk blocking the stream in order to reference it.
|
||||
if (streamInfo.isBlocked())
|
||||
{
|
||||
streamInfo.getCurrentSectionInfo().block();
|
||||
sectionInfo.block();
|
||||
sectionInfo.reference(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_blockedStreams < _maxBlockedStreams)
|
||||
{
|
||||
_blockedStreams++;
|
||||
streamInfo.getCurrentSectionInfo().block();
|
||||
sectionInfo.block();
|
||||
sectionInfo.reference(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -259,7 +268,6 @@ public class QpackEncoder
|
|||
synchronized (this)
|
||||
{
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
dynamicTable.evict();
|
||||
|
||||
StreamInfo streamInfo = _streamInfoMap.get(streamId);
|
||||
if (streamInfo == null)
|
||||
|
@ -285,6 +293,7 @@ public class QpackEncoder
|
|||
}
|
||||
}
|
||||
|
||||
sectionInfo.setRequiredInsertCount(requiredInsertCount);
|
||||
base = dynamicTable.getBase();
|
||||
encodedInsertCount = encodeInsertCount(requiredInsertCount, dynamicTable.getCapacity());
|
||||
signBit = base < requiredInsertCount;
|
||||
|
@ -310,7 +319,7 @@ public class QpackEncoder
|
|||
return buffer;
|
||||
}
|
||||
|
||||
private EncodableEntry encode(StreamInfo streamInfo, HttpField field)
|
||||
private EncodableEntry encode(StreamInfo streamInfo, HttpField field) throws QpackException
|
||||
{
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
|
||||
|
@ -322,21 +331,22 @@ public class QpackEncoder
|
|||
if (field instanceof PreEncodedHttpField)
|
||||
return EncodableEntry.getPreEncodedEntry((PreEncodedHttpField)field);
|
||||
|
||||
boolean canCreateEntry = shouldIndex(field) && (Entry.getSize(field) <= dynamicTable.getSpace());
|
||||
boolean canCreateEntry = shouldIndex(field) && dynamicTable.canInsert(field);
|
||||
|
||||
Entry entry = _context.get(field);
|
||||
if (entry != null && referenceEntry(entry, streamInfo))
|
||||
if (referenceEntry(entry, streamInfo))
|
||||
{
|
||||
return EncodableEntry.getReferencedEntry(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Should we duplicate this entry.
|
||||
if (entry != null && dynamicTable.canReference(entry) && canCreateEntry)
|
||||
if (entry != null && canCreateEntry)
|
||||
{
|
||||
int index = _context.indexOf(entry);
|
||||
Entry newEntry = new Entry(field);
|
||||
dynamicTable.add(newEntry);
|
||||
_handler.onInstruction(new DuplicateInstruction(entry.getIndex()));
|
||||
_handler.onInstruction(new DuplicateInstruction(index));
|
||||
|
||||
// Should we reference this entry and risk blocking.
|
||||
if (referenceEntry(newEntry, streamInfo))
|
||||
|
@ -346,14 +356,15 @@ public class QpackEncoder
|
|||
|
||||
boolean huffman = shouldHuffmanEncode(field);
|
||||
Entry nameEntry = _context.get(field.getName());
|
||||
if (nameEntry != null && referenceEntry(nameEntry, streamInfo))
|
||||
if (referenceEntry(nameEntry, streamInfo))
|
||||
{
|
||||
// Should we copy this entry
|
||||
if (canCreateEntry)
|
||||
{
|
||||
int index = _context.indexOf(nameEntry);
|
||||
Entry newEntry = new Entry(field);
|
||||
dynamicTable.add(newEntry);
|
||||
_handler.onInstruction(new IndexedNameEntryInstruction(!nameEntry.isStatic(), nameEntry.getIndex(), huffman, field.getValue()));
|
||||
_handler.onInstruction(new IndexedNameEntryInstruction(!nameEntry.isStatic(), index, huffman, field.getValue()));
|
||||
|
||||
// Should we reference this entry and risk blocking.
|
||||
if (referenceEntry(newEntry, streamInfo))
|
||||
|
@ -379,38 +390,40 @@ public class QpackEncoder
|
|||
}
|
||||
}
|
||||
|
||||
public boolean insert(HttpField field)
|
||||
public boolean insert(HttpField field) throws QpackException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
DynamicTable dynamicTable = _context.getDynamicTable();
|
||||
dynamicTable.evict();
|
||||
|
||||
if (field.getValue() == null)
|
||||
field = new HttpField(field.getHeader(), field.getName(), "");
|
||||
|
||||
boolean canCreateEntry = shouldIndex(field) && (Entry.getSize(field) <= dynamicTable.getSpace());
|
||||
boolean canCreateEntry = shouldIndex(field) && dynamicTable.canInsert(field);
|
||||
if (!canCreateEntry)
|
||||
return false;
|
||||
|
||||
Entry newEntry = new Entry(field);
|
||||
dynamicTable.add(newEntry);
|
||||
|
||||
// We can always reference on insertion as it will always arrive before any eviction.
|
||||
Entry entry = _context.get(field);
|
||||
if (referenceEntry(entry))
|
||||
if (entry != null)
|
||||
{
|
||||
_handler.onInstruction(new DuplicateInstruction(entry.getIndex()));
|
||||
int index = _context.indexOf(entry);
|
||||
dynamicTable.add(new Entry(field));
|
||||
_handler.onInstruction(new DuplicateInstruction(index));
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean huffman = shouldHuffmanEncode(field);
|
||||
Entry nameEntry = _context.get(field.getName());
|
||||
if (referenceEntry(nameEntry))
|
||||
if (nameEntry != null)
|
||||
{
|
||||
_handler.onInstruction(new IndexedNameEntryInstruction(!nameEntry.isStatic(), nameEntry.getIndex(), huffman, field.getValue()));
|
||||
int index = _context.indexOf(nameEntry);
|
||||
dynamicTable.add(new Entry(field));
|
||||
_handler.onInstruction(new IndexedNameEntryInstruction(!nameEntry.isStatic(), index, huffman, field.getValue()));
|
||||
return true;
|
||||
}
|
||||
|
||||
dynamicTable.add(new Entry(field));
|
||||
_handler.onInstruction(new LiteralNameEntryInstruction(huffman, field.getName(), huffman, field.getValue()));
|
||||
return true;
|
||||
}
|
||||
|
@ -424,4 +437,34 @@ public class QpackEncoder
|
|||
int maxEntries = maxTableCapacity / 32;
|
||||
return (reqInsertCount % (2 * maxEntries)) + 1;
|
||||
}
|
||||
|
||||
public class EncoderAdapter implements EncoderInstructionParser.Handler
|
||||
{
|
||||
@Override
|
||||
public void onSectionAcknowledgement(int streamId) throws QpackException
|
||||
{
|
||||
sectionAcknowledgement(streamId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamCancellation(int streamId) throws QpackException
|
||||
{
|
||||
streamCancellation(streamId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInsertCountIncrement(int increment) throws QpackException
|
||||
{
|
||||
insertCountIncrement(increment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Appendable out, String indent) throws IOException
|
||||
{
|
||||
synchronized (this)
|
||||
{
|
||||
Dumpable.dumpObjects(out, indent, _context.getDynamicTable());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,10 +13,15 @@
|
|||
|
||||
package org.eclipse.jetty.http3.qpack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
class StreamInfo
|
||||
import org.eclipse.jetty.http3.qpack.table.Entry;
|
||||
|
||||
class StreamInfo implements Iterable<StreamInfo.SectionInfo>
|
||||
{
|
||||
private final int _streamId;
|
||||
private final Queue<SectionInfo> _sectionInfos = new LinkedList<>();
|
||||
|
@ -62,8 +67,15 @@ class StreamInfo
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<SectionInfo> iterator()
|
||||
{
|
||||
return _sectionInfos.iterator();
|
||||
}
|
||||
|
||||
public static class SectionInfo
|
||||
{
|
||||
private final List<Entry> _entries = new ArrayList<>();
|
||||
private int _requiredInsertCount;
|
||||
private boolean _block = false;
|
||||
|
||||
|
@ -77,6 +89,21 @@ class StreamInfo
|
|||
return _block;
|
||||
}
|
||||
|
||||
public void reference(Entry entry)
|
||||
{
|
||||
entry.reference();
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void release()
|
||||
{
|
||||
for (Entry entry : _entries)
|
||||
{
|
||||
entry.release();
|
||||
}
|
||||
_entries.clear();
|
||||
}
|
||||
|
||||
public void setRequiredInsertCount(int requiredInsertCount)
|
||||
{
|
||||
_requiredInsertCount = requiredInsertCount;
|
||||
|
|
|
@ -15,7 +15,6 @@ 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;
|
||||
|
||||
/**
|
||||
|
@ -61,45 +60,6 @@ public class DecoderInstructionParser
|
|||
void onInsertWithLiteralName(String name, String value) throws QpackException;
|
||||
}
|
||||
|
||||
public static class DecoderAdapter implements Handler
|
||||
{
|
||||
private final QpackDecoder _decoder;
|
||||
|
||||
public DecoderAdapter(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 DecoderInstructionParser(QpackDecoder decoder)
|
||||
{
|
||||
this(new DecoderAdapter(decoder));
|
||||
}
|
||||
|
||||
public DecoderInstructionParser(Handler handler)
|
||||
{
|
||||
_handler = handler;
|
||||
|
|
|
@ -118,7 +118,7 @@ public class EncodedFieldSection
|
|||
byte firstByte = buffer.get(buffer.position());
|
||||
boolean allowEncoding = (firstByte & 0x10) != 0;
|
||||
|
||||
_stringParser.setPrefix(3);
|
||||
_stringParser.setPrefix(4);
|
||||
String name = _stringParser.decode(buffer);
|
||||
if (name == null)
|
||||
throw new QpackException.CompressionException("Invalid Name");
|
||||
|
@ -136,7 +136,7 @@ public class EncodedFieldSection
|
|||
byte firstByte = buffer.get(buffer.position());
|
||||
boolean allowEncoding = (firstByte & 0x08) != 0;
|
||||
|
||||
_integerParser.setPrefix(3);
|
||||
_integerParser.setPrefix(4);
|
||||
int nameIndex = _integerParser.decode(buffer);
|
||||
if (nameIndex < 0)
|
||||
throw new QpackException.CompressionException("Invalid Index");
|
||||
|
@ -199,9 +199,9 @@ public class EncodedFieldSection
|
|||
public HttpField decode(QpackContext context) throws QpackException
|
||||
{
|
||||
if (_dynamicTable)
|
||||
return context.getDynamicTable().getAbsolute(_base + _index).getHttpField();
|
||||
return context.getDynamicTable().getAbsolute(_base - (_index + 1)).getHttpField();
|
||||
else
|
||||
return context.getStaticTable().get(_index).getHttpField();
|
||||
return QpackContext.getStaticTable().get(_index).getHttpField();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -217,7 +217,7 @@ public class EncodedFieldSection
|
|||
@Override
|
||||
public HttpField decode(QpackContext context) throws QpackException
|
||||
{
|
||||
return context.getDynamicTable().getAbsolute(_base - _index).getHttpField();
|
||||
return context.getDynamicTable().getAbsolute(_base + _index).getHttpField();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,7 +242,7 @@ public class EncodedFieldSection
|
|||
{
|
||||
HttpField field;
|
||||
if (_dynamicTable)
|
||||
field = context.getDynamicTable().getAbsolute(_base + _nameIndex + 1).getHttpField();
|
||||
field = context.getDynamicTable().getAbsolute(_base - (_nameIndex + 1)).getHttpField();
|
||||
else
|
||||
field = QpackContext.getStaticTable().get(_nameIndex).getHttpField();
|
||||
|
||||
|
@ -266,7 +266,7 @@ public class EncodedFieldSection
|
|||
@Override
|
||||
public HttpField decode(QpackContext context) throws QpackException
|
||||
{
|
||||
HttpField field = context.getDynamicTable().getAbsolute(_base - _nameIndex).getHttpField();
|
||||
HttpField field = context.getDynamicTable().getAbsolute(_base + _nameIndex).getHttpField();
|
||||
return new HttpField(field.getHeader(), field.getName(), _value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ package org.eclipse.jetty.http3.qpack.parser;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.eclipse.jetty.http3.qpack.QpackEncoder;
|
||||
import org.eclipse.jetty.http3.qpack.QpackException;
|
||||
|
||||
/**
|
||||
|
@ -48,39 +47,6 @@ public class EncoderInstructionParser
|
|||
void onInsertCountIncrement(int increment) throws QpackException;
|
||||
}
|
||||
|
||||
public static class EncoderAdapter implements Handler
|
||||
{
|
||||
private final QpackEncoder _encoder;
|
||||
|
||||
public EncoderAdapter(QpackEncoder encoder)
|
||||
{
|
||||
_encoder = encoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSectionAcknowledgement(int streamId) throws QpackException
|
||||
{
|
||||
_encoder.sectionAcknowledgement(streamId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamCancellation(int streamId) throws QpackException
|
||||
{
|
||||
_encoder.streamCancellation(streamId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInsertCountIncrement(int increment) throws QpackException
|
||||
{
|
||||
_encoder.insertCountIncrement(increment);
|
||||
}
|
||||
}
|
||||
|
||||
public EncoderInstructionParser(QpackEncoder encoder)
|
||||
{
|
||||
this(new EncoderAdapter(encoder));
|
||||
}
|
||||
|
||||
public EncoderInstructionParser(Handler handler)
|
||||
{
|
||||
_handler = handler;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
package org.eclipse.jetty.http3.qpack.table;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
@ -20,10 +21,10 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http3.qpack.QpackContext;
|
||||
import org.eclipse.jetty.http3.qpack.QpackException;
|
||||
import org.eclipse.jetty.util.component.Dumpable;
|
||||
|
||||
public class DynamicTable implements Iterable<Entry>
|
||||
public class DynamicTable implements Iterable<Entry>, Dumpable
|
||||
{
|
||||
private final Map<HttpField, Entry> _fieldMap = new HashMap<>();
|
||||
private final Map<String, Entry> _nameMap = new HashMap<>();
|
||||
|
@ -34,9 +35,38 @@ public class DynamicTable implements Iterable<Entry>
|
|||
private int _absoluteIndex;
|
||||
private int _drainingIndex;
|
||||
|
||||
/**
|
||||
* Add an entry into the Dynamic Table. This will throw if it is not possible to insert this entry.
|
||||
* Use {@link #canInsert(HttpField)} to check whether it is possible to insert this entry.
|
||||
* @param entry the entry to insert.
|
||||
*/
|
||||
public void add(Entry entry)
|
||||
{
|
||||
// Evict entries until there is space for the new entry.
|
||||
Iterator<Entry> iterator = _entries.iterator();
|
||||
int entrySize = entry.getSize();
|
||||
while (getSpace() < entrySize)
|
||||
{
|
||||
if (!iterator.hasNext())
|
||||
throw new IllegalStateException("not enough space in dynamic table to add entry");
|
||||
|
||||
Entry e = iterator.next();
|
||||
if (e.getReferenceCount() != 0)
|
||||
throw new IllegalStateException("cannot evict entry that is still referenced");
|
||||
|
||||
// Evict the entry from the DynamicTable.
|
||||
_size -= e.getSize();
|
||||
iterator.remove();
|
||||
|
||||
HttpField httpField = e.getHttpField();
|
||||
if (e == _fieldMap.get(httpField))
|
||||
_fieldMap.remove(httpField);
|
||||
|
||||
String name = httpField.getLowerCaseName();
|
||||
if (e == _nameMap.get(name))
|
||||
_nameMap.remove(name);
|
||||
}
|
||||
|
||||
if (entrySize + _size > _capacity)
|
||||
throw new IllegalStateException("No available space");
|
||||
_size += entrySize;
|
||||
|
@ -51,16 +81,73 @@ public class DynamicTable implements Iterable<Entry>
|
|||
_drainingIndex = getDrainingIndex();
|
||||
}
|
||||
|
||||
public int index(Entry entry)
|
||||
/**
|
||||
* Is there enough room to insert a new entry into the Dynamic Table, possibly evicting unreferenced entries.
|
||||
* @param field the HttpField to insert into the table.
|
||||
* @return if an entry with this HttpField can be inserted.
|
||||
*/
|
||||
public boolean canInsert(HttpField field)
|
||||
{
|
||||
return _entries.indexOf(entry);
|
||||
int availableSpace = getSpace();
|
||||
int requiredSpace = Entry.getSize(field);
|
||||
|
||||
if (availableSpace >= requiredSpace)
|
||||
return true;
|
||||
|
||||
if (requiredSpace > _capacity)
|
||||
return false;
|
||||
|
||||
// Could we potentially evict enough space to insert this new field.
|
||||
for (Entry entry : _entries)
|
||||
{
|
||||
if (entry.getReferenceCount() != 0)
|
||||
return false;
|
||||
|
||||
availableSpace += entry.getSize();
|
||||
if (availableSpace >= requiredSpace)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relative index of an entry in the Dynamic Table.
|
||||
* @param entry the entry to find the relative index of.
|
||||
* @return the relative index of this entry.
|
||||
*/
|
||||
public int index(Entry entry)
|
||||
{
|
||||
if (_entries.isEmpty())
|
||||
throw new IllegalArgumentException("Invalid Index");
|
||||
|
||||
Entry firstEntry = _entries.get(0);
|
||||
int index = entry.getIndex() - firstEntry.getIndex();
|
||||
if (index >= _entries.size())
|
||||
throw new IllegalArgumentException("Invalid Index");
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entry from the Dynamic table given an absolute index.
|
||||
* @param absoluteIndex the absolute index of the entry in the table.
|
||||
* @return the entry with the absolute index.
|
||||
*/
|
||||
public Entry getAbsolute(int absoluteIndex) throws QpackException
|
||||
{
|
||||
if (absoluteIndex < 0)
|
||||
throw new QpackException.CompressionException("Invalid Index");
|
||||
return _entries.stream().filter(e -> e.getIndex() == absoluteIndex).findFirst().orElse(null);
|
||||
|
||||
if (_entries.isEmpty())
|
||||
throw new IllegalArgumentException("Invalid Index");
|
||||
|
||||
Entry firstEntry = _entries.get(0);
|
||||
int index = absoluteIndex - firstEntry.getIndex();
|
||||
if (index >= _entries.size())
|
||||
throw new IllegalArgumentException("Invalid Index");
|
||||
|
||||
return _entries.get(index);
|
||||
}
|
||||
|
||||
public Entry get(int index)
|
||||
|
@ -112,31 +199,21 @@ public class DynamicTable implements Iterable<Entry>
|
|||
|
||||
public void setCapacity(int capacity)
|
||||
{
|
||||
if (QpackContext.LOG.isDebugEnabled())
|
||||
QpackContext.LOG.debug(String.format("HdrTbl[%x] resized max=%d->%d", hashCode(), _capacity, capacity));
|
||||
_capacity = capacity;
|
||||
}
|
||||
|
||||
public boolean canReference(Entry entry)
|
||||
Iterator<Entry> iterator = _entries.iterator();
|
||||
while (_size > _capacity)
|
||||
{
|
||||
return entry.getIndex() >= _drainingIndex;
|
||||
}
|
||||
if (!iterator.hasNext())
|
||||
throw new IllegalStateException();
|
||||
|
||||
public void evict()
|
||||
{
|
||||
for (Entry entry : _entries)
|
||||
{
|
||||
if (entry.getIndex() >= _drainingIndex)
|
||||
return;
|
||||
|
||||
// We can only evict if there are no references outstanding to this entry.
|
||||
Entry entry = iterator.next();
|
||||
if (entry.getReferenceCount() != 0)
|
||||
return;
|
||||
|
||||
// Evict the entry from the DynamicTable.
|
||||
_size -= entry.getSize();
|
||||
if (entry != _entries.remove(0))
|
||||
throw new IllegalStateException("Corruption in DynamicTable");
|
||||
iterator.remove();
|
||||
|
||||
HttpField httpField = entry.getHttpField();
|
||||
if (entry == _fieldMap.get(httpField))
|
||||
|
@ -148,6 +225,11 @@ public class DynamicTable implements Iterable<Entry>
|
|||
}
|
||||
}
|
||||
|
||||
public boolean canReference(Entry entry)
|
||||
{
|
||||
return entry.getIndex() >= _drainingIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entries with indexes lower than the draining index should not be referenced.
|
||||
* This allows these entries to eventually be evicted as no more references will be made to them.
|
||||
|
@ -183,6 +265,12 @@ public class DynamicTable implements Iterable<Entry>
|
|||
return _entries.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Appendable out, String indent) throws IOException
|
||||
{
|
||||
Dumpable.dumpObjects(out, indent, _entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
|
@ -65,6 +65,11 @@ public class Entry
|
|||
_referenceCount.incrementAndGet();
|
||||
}
|
||||
|
||||
public void release()
|
||||
{
|
||||
_referenceCount.decrementAndGet();
|
||||
}
|
||||
|
||||
public int getReferenceCount()
|
||||
{
|
||||
return _referenceCount.get();
|
||||
|
@ -83,7 +88,13 @@ public class Entry
|
|||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("{%s,%d,%s,%x}", isStatic() ? "S" : "D", _absoluteIndex, _field, hashCode());
|
||||
return String.format("%s@%x{index=%d, refs=%d, field=\"%s\"}", getClass().getSimpleName(), hashCode(),
|
||||
_absoluteIndex, _referenceCount.get(), _field);
|
||||
}
|
||||
|
||||
public static int getSize(Entry entry)
|
||||
{
|
||||
return getSize(entry.getHttpField());
|
||||
}
|
||||
|
||||
public static int getSize(HttpField field)
|
||||
|
|
|
@ -25,11 +25,7 @@ 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;
|
||||
|
||||
|
@ -66,8 +62,6 @@ public class EncodeDecodeTest
|
|||
}
|
||||
};
|
||||
_decoder = new QpackDecoder(_decoderHandler, MAX_HEADER_SIZE);
|
||||
_encoderInstructionParser = new EncoderInstructionParser(_encoder);
|
||||
_decoderInstructionParser = new DecoderInstructionParser(_decoder);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -79,7 +73,7 @@ public class EncodeDecodeTest
|
|||
|
||||
ByteBuffer buffer = _encoder.encode(streamId, httpFields);
|
||||
assertNull(_encoderHandler.getInstruction());
|
||||
assertThat(BufferUtil.toHexString(buffer), equalsHex("0000 510b 2f69 6e64 6578 2e68 746d 6c"));
|
||||
assertThat(BufferUtil.toHexString(buffer), QpackTestUtil.equalsHex("0000 510b 2f69 6e64 6578 2e68 746d 6c"));
|
||||
assertTrue(_encoderHandler.isEmpty());
|
||||
|
||||
_decoder.decode(streamId, buffer);
|
||||
|
@ -88,7 +82,7 @@ public class EncodeDecodeTest
|
|||
assertThat(_decoderHandler.getInstruction(), instanceOf(SectionAcknowledgmentInstruction.class));
|
||||
assertTrue(_decoderHandler.isEmpty());
|
||||
|
||||
_encoderInstructionParser.parse(toBuffer(new SectionAcknowledgmentInstruction(streamId)));
|
||||
_encoderInstructionParser.parse(QpackTestUtil.toBuffer(new SectionAcknowledgmentInstruction(streamId)));
|
||||
|
||||
// B.2. Dynamic Table.
|
||||
|
||||
|
@ -97,9 +91,9 @@ public class EncodeDecodeTest
|
|||
Instruction instruction = _encoderHandler.getInstruction();
|
||||
assertThat(instruction, instanceOf(SetCapacityInstruction.class));
|
||||
assertThat(((SetCapacityInstruction)instruction).getCapacity(), is(220));
|
||||
assertThat(toString(instruction), equalsHex("3fbd01"));
|
||||
assertThat(QpackTestUtil.toHexString(instruction), QpackTestUtil.equalsHex("3fbd01"));
|
||||
|
||||
_decoderInstructionParser.parse(toHex("3fbd01"));
|
||||
_decoderInstructionParser.parse(QpackTestUtil.hexToBuffer("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.
|
||||
|
@ -113,24 +107,24 @@ public class EncodeDecodeTest
|
|||
assertThat(instruction, instanceOf(IndexedNameEntryInstruction.class));
|
||||
assertThat(((IndexedNameEntryInstruction)instruction).getIndex(), is(0));
|
||||
assertThat(((IndexedNameEntryInstruction)instruction).getValue(), is("www.example.com"));
|
||||
assertThat(toString(instruction), equalsHex("c00f 7777 772e 6578 616d 706c 652e 636f 6d"));
|
||||
assertThat(QpackTestUtil.toHexString(instruction), QpackTestUtil.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(toString(instruction), equalsHex("c10c 2f73 616d 706c 652f 7061 7468"));
|
||||
assertThat(QpackTestUtil.toHexString(instruction), QpackTestUtil.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(streamId, buffer);
|
||||
assertNull(_decoderHandler.getHttpFields());
|
||||
|
||||
_decoderInstructionParser.parse(toHex("c00f 7777 772e 6578 616d 706c 652e 636f 6d"));
|
||||
_decoderInstructionParser.parse(QpackTestUtil.hexToBuffer("c00f 7777 772e 6578 616d 706c 652e 636f 6d"));
|
||||
assertNull(_decoderHandler.getHttpFields());
|
||||
assertThat(_decoderHandler.getInstruction(), instanceOf(InsertCountIncrementInstruction.class));
|
||||
|
||||
_decoderInstructionParser.parse(toHex("c10c 2f73 616d 706c 652f 7061 7468"));
|
||||
_decoderInstructionParser.parse(QpackTestUtil.hexToBuffer("c10c 2f73 616d 706c 652f 7061 7468"));
|
||||
assertThat(_decoderHandler.getHttpFields(), is(httpFields));
|
||||
assertThat(_decoderHandler.getInstruction(), instanceOf(InsertCountIncrementInstruction.class));
|
||||
|
||||
|
@ -138,39 +132,14 @@ public class EncodeDecodeTest
|
|||
assertTrue(_decoderHandler.isEmpty());
|
||||
|
||||
// Parse the decoder instructions on the encoder.
|
||||
_encoderInstructionParser.parse(toBuffer(new InsertCountIncrementInstruction(2)));
|
||||
_encoderInstructionParser.parse(toBuffer(new SectionAcknowledgmentInstruction(streamId)));
|
||||
_encoderInstructionParser.parse(QpackTestUtil.toBuffer(new InsertCountIncrementInstruction(2)));
|
||||
_encoderInstructionParser.parse(QpackTestUtil.toBuffer(new SectionAcknowledgmentInstruction(streamId)));
|
||||
|
||||
// B.3. Speculative Insert
|
||||
_encoder.insert(new HttpField("custom-key", "custom-value"));
|
||||
instruction = _encoderHandler.getInstruction();
|
||||
assertThat(instruction, instanceOf(LiteralNameEntryInstruction.class));
|
||||
assertThat(toString(instruction), equalsHex("4a63 7573 746f 6d2d 6b65 790c 6375 7374 6f6d 2d76 616c 7565"));
|
||||
assertThat(QpackTestUtil.toHexString(instruction), QpackTestUtil.equalsHex("4a63 7573 746f 6d2d 6b65 790c 6375 7374 6f6d 2d76 616c 7565"));
|
||||
_encoder.insertCountIncrement(1);
|
||||
}
|
||||
|
||||
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 String toString(Instruction instruction)
|
||||
{
|
||||
return BufferUtil.toHexString(toBuffer(instruction));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.Random;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
public class EvictionTest
|
||||
{
|
||||
private QpackEncoder _encoder;
|
||||
private QpackDecoder _decoder;
|
||||
private final TestDecoderHandler _decoderHandler = new TestDecoderHandler();
|
||||
private final TestEncoderHandler _encoderHandler = new TestEncoderHandler();
|
||||
private final Random random = new Random();
|
||||
|
||||
private static final int MAX_BLOCKED_STREAMS = 5;
|
||||
private static final int MAX_HEADER_SIZE = 1024;
|
||||
|
||||
@BeforeEach
|
||||
public void before()
|
||||
{
|
||||
_decoder = new QpackDecoder(_decoderHandler, MAX_HEADER_SIZE);
|
||||
_encoder = new QpackEncoder(_encoderHandler, MAX_BLOCKED_STREAMS)
|
||||
{
|
||||
@Override
|
||||
protected boolean shouldHuffmanEncode(HttpField httpField)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Set the instruction bytes to be passed on to the remote Encoder/Decoder through the handler directly.
|
||||
_encoderHandler.setDecoder(_decoder);
|
||||
_decoderHandler.setEncoder(_encoder);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() throws Exception
|
||||
{
|
||||
_encoder.setCapacity(1024);
|
||||
|
||||
for (int i = 0; i < 10000; i++)
|
||||
{
|
||||
HttpFields httpFields = newRandomFields(5);
|
||||
int streamId = getPositiveInt(10);
|
||||
ByteBuffer encodedFields = _encoder.encode(streamId, httpFields);
|
||||
_decoder.decode(streamId, encodedFields);
|
||||
HttpFields result = _decoderHandler.getHttpFields();
|
||||
|
||||
System.err.println("encoder: ");
|
||||
System.err.println(_encoder.dump());
|
||||
System.err.println();
|
||||
System.err.println("decoder: ");
|
||||
System.err.println(_decoder.dump());
|
||||
System.err.println();
|
||||
System.err.println("====================");
|
||||
System.err.println();
|
||||
|
||||
assertThat(result, is(httpFields));
|
||||
}
|
||||
}
|
||||
|
||||
public HttpFields newRandomFields(int size)
|
||||
{
|
||||
HttpFields.Mutable fields = HttpFields.build();
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
fields.add(newRandomField());
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
public HttpField newRandomField()
|
||||
{
|
||||
String header = "header" + getPositiveInt(999);
|
||||
String value = "value" + getPositiveInt(999);
|
||||
return new HttpField(header, value);
|
||||
}
|
||||
|
||||
public int getPositiveInt(int max)
|
||||
{
|
||||
return Math.abs(random.nextInt(max));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.http3.qpack.generator.Instruction;
|
||||
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 static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
public class QpackTestUtil
|
||||
{
|
||||
public static Matcher<String> equalsHex(String expectedString)
|
||||
{
|
||||
expectedString = expectedString.replaceAll("\\s+", "");
|
||||
return org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase(expectedString);
|
||||
}
|
||||
|
||||
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 hexToBuffer(String hexString)
|
||||
{
|
||||
hexString = hexString.replaceAll("\\s+", "");
|
||||
return ByteBuffer.wrap(TypeUtil.fromHexString(hexString));
|
||||
}
|
||||
|
||||
public static String toHexString(Instruction instruction)
|
||||
{
|
||||
return BufferUtil.toHexString(toBuffer(instruction));
|
||||
}
|
||||
}
|
|
@ -23,6 +23,12 @@ public class TestDecoderHandler implements QpackDecoder.Handler
|
|||
{
|
||||
private final Queue<HttpFields> _httpFieldsList = new LinkedList<>();
|
||||
private final Queue<Instruction> _instructionList = new LinkedList<>();
|
||||
private QpackEncoder _encoder;
|
||||
|
||||
public void setEncoder(QpackEncoder encoder)
|
||||
{
|
||||
_encoder = encoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHttpFields(int streamId, HttpFields httpFields)
|
||||
|
@ -31,9 +37,11 @@ public class TestDecoderHandler implements QpackDecoder.Handler
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onInstruction(Instruction instruction)
|
||||
public void onInstruction(Instruction instruction) throws QpackException
|
||||
{
|
||||
_instructionList.add(instruction);
|
||||
if (_encoder != null)
|
||||
_encoder.parseInstruction(QpackTestUtil.toBuffer(instruction));
|
||||
}
|
||||
|
||||
public HttpFields getHttpFields()
|
||||
|
|
|
@ -21,11 +21,19 @@ import org.eclipse.jetty.http3.qpack.generator.Instruction;
|
|||
public class TestEncoderHandler implements QpackEncoder.Handler
|
||||
{
|
||||
private final Queue<Instruction> _instructionList = new LinkedList<>();
|
||||
private QpackDecoder _decoder;
|
||||
|
||||
public void setDecoder(QpackDecoder decoder)
|
||||
{
|
||||
_decoder = decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstruction(Instruction instruction)
|
||||
public void onInstruction(Instruction instruction) throws QpackException
|
||||
{
|
||||
_instructionList.add(instruction);
|
||||
if (_decoder != null)
|
||||
_decoder.parseInstruction(QpackTestUtil.toBuffer(instruction));
|
||||
}
|
||||
|
||||
public Instruction getInstruction()
|
||||
|
|
Loading…
Reference in New Issue