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:
parent
5b178d16b7
commit
bdf44b8e22
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -24,6 +24,8 @@ public class NBitIntegerParser
|
|||
|
||||
public void setPrefix(int prefix)
|
||||
{
|
||||
if (_started)
|
||||
throw new IllegalStateException();
|
||||
_prefix = prefix;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,8 @@ public class NBitStringParser
|
|||
|
||||
public void setPrefix(int prefix)
|
||||
{
|
||||
if (_state != State.PARSING)
|
||||
throw new IllegalStateException();
|
||||
_prefix = prefix;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -30,68 +30,105 @@ 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}
|
||||
{":authority", ""},
|
||||
{":path", "/"},
|
||||
{"age", "0"},
|
||||
{"content-disposition", ""},
|
||||
{"content-length", "0"},
|
||||
{"cookie", ""},
|
||||
{"date", ""},
|
||||
{"etag", ""},
|
||||
{"if-modified-since", ""},
|
||||
{"if-none-match", ""},
|
||||
{"last-modified", ""},
|
||||
{"link", ""},
|
||||
{"location", ""},
|
||||
{"referer", ""},
|
||||
{"set-cookie", ""},
|
||||
{":method", "CONNECT"},
|
||||
{":method", "DELETE"},
|
||||
{":method", "GET"},
|
||||
{":method", "HEAD"},
|
||||
{":method", "OPTIONS"},
|
||||
{":method", "POST"},
|
||||
{":method", "PUT"},
|
||||
{":scheme", "http"},
|
||||
{":scheme", "https"},
|
||||
{":status", "103"},
|
||||
{":status", "200"},
|
||||
{":status", "304"},
|
||||
{":status", "404"},
|
||||
{":status", "503"},
|
||||
{"accept", "*/*"},
|
||||
{"accept", "application/dns-message"},
|
||||
{"accept-encoding", "gzip, deflate, br"},
|
||||
{"accept-ranges", "bytes"},
|
||||
{"access-control-allow-headers", "cache-control"},
|
||||
{"access-control-allow-headers", "content-type"},
|
||||
{"access-control-allow-origin", "*"},
|
||||
{"cache-control", "max-age=0"},
|
||||
{"cache-control", "max-age=2592000"},
|
||||
{"cache-control", "max-age=604800"},
|
||||
{"cache-control", "no-cache"},
|
||||
{"cache-control", "no-store"},
|
||||
{"cache-control", "public, max-age=31536000"},
|
||||
{"content-encoding", "br"},
|
||||
{"content-encoding", "gzip"},
|
||||
{"content-type", "application/dns-message"},
|
||||
{"content-type", "application/javascript"},
|
||||
{"content-type", "application/json"},
|
||||
{"content-type", "application/x-www-form-urlencoded"},
|
||||
{"content-type", "image/gif"},
|
||||
{"content-type", "image/jpeg"},
|
||||
{"content-type", "image/png"},
|
||||
{"content-type", "text/css"},
|
||||
{"content-type", "text/html; charset=utf-8"},
|
||||
{"content-type", "text/plain"},
|
||||
{"content-type", "text/plain;charset=utf-8"},
|
||||
{"range", "bytes=0-"},
|
||||
{"strict-transport-security", "max-age=31536000"},
|
||||
{"strict-transport-security", "max-age=31536000; includesubdomains"},
|
||||
{"strict-transport-security", "max-age=31536000; includesubdomains; preload"},
|
||||
{"vary", "accept-encoding"},
|
||||
{"vary", "origin"},
|
||||
{"x-content-type-options", "nosniff"},
|
||||
{"x-xss-protection", "1; mode=block"},
|
||||
{":status", "100"},
|
||||
{":status", "204"},
|
||||
{":status", "206"},
|
||||
{":status", "302"},
|
||||
{":status", "400"},
|
||||
{":status", "403"},
|
||||
{":status", "421"},
|
||||
{":status", "425"},
|
||||
{":status", "500"},
|
||||
{"accept-language", ""},
|
||||
{"access-control-allow-credentials", "FALSE"},
|
||||
{"access-control-allow-credentials", "TRUE"},
|
||||
{"access-control-allow-headers", "*"},
|
||||
{"access-control-allow-methods", "get"},
|
||||
{"access-control-allow-methods", "get, post, options"},
|
||||
{"access-control-allow-methods", "options"},
|
||||
{"access-control-expose-headers", "content-length"},
|
||||
{"access-control-request-headers", "content-type"},
|
||||
{"access-control-request-method", "get"},
|
||||
{"access-control-request-method", "post"},
|
||||
{"alt-svc", "clear"},
|
||||
{"authorization", ""},
|
||||
{"content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"},
|
||||
{"early-data", "1"},
|
||||
{"expect-ct", ""},
|
||||
{"forwarded", ""},
|
||||
{"if-range", ""},
|
||||
{"origin", ""},
|
||||
{"purpose", "prefetch"},
|
||||
{"server", ""},
|
||||
{"timing-allow-origin", "*"},
|
||||
{"upgrade-insecure-requests", "1"},
|
||||
{"user-agent", ""},
|
||||
{"x-forwarded-for", ""},
|
||||
{"x-frame-options", "deny"},
|
||||
{"x-frame-options", "sameorigin"},
|
||||
};
|
||||
|
||||
public static final int STATIC_SIZE = STATIC_TABLE.length - 1;
|
||||
|
@ -105,7 +142,7 @@ public class 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++)
|
||||
for (int i = 0; i < STATIC_TABLE.length; i++)
|
||||
{
|
||||
StaticEntry entry = null;
|
||||
|
||||
|
@ -193,108 +230,4 @@ public class StaticTable
|
|||
return null;
|
||||
return _staticTableByHeader[index];
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final String[][] QPACK_STATIC_TABLE =
|
||||
{
|
||||
{":authority", ""},
|
||||
{":path", "/"},
|
||||
{"age", "0"},
|
||||
{"content-disposition", ""},
|
||||
{"content-length", "0"},
|
||||
{"cookie", ""},
|
||||
{"date", ""},
|
||||
{"etag", ""},
|
||||
{"if-modified-since", ""},
|
||||
{"if-none-match", ""},
|
||||
{"last-modified", ""},
|
||||
{"link", ""},
|
||||
{"location", ""},
|
||||
{"referer", ""},
|
||||
{"set-cookie", ""},
|
||||
{":method", "CONNECT"},
|
||||
{":method", "DELETE"},
|
||||
{":method", "GET"},
|
||||
{":method", "HEAD"},
|
||||
{":method", "OPTIONS"},
|
||||
{":method", "POST"},
|
||||
{":method", "PUT"},
|
||||
{":scheme", "http"},
|
||||
{":scheme", "https"},
|
||||
{":status", "103"},
|
||||
{":status", "200"},
|
||||
{":status", "304"},
|
||||
{":status", "404"},
|
||||
{":status", "503"},
|
||||
{"accept", "*/*"},
|
||||
{"accept", "application/dns-message"},
|
||||
{"accept-encoding", "gzip, deflate, br"},
|
||||
{"accept-ranges", "bytes"},
|
||||
{"access-control-allow-headers", "cache-control"},
|
||||
{"access-control-allow-headers", "content-type"},
|
||||
{"access-control-allow-origin", "*"},
|
||||
{"cache-control", "max-age=0"},
|
||||
{"cache-control", "max-age=2592000"},
|
||||
{"cache-control", "max-age=604800"},
|
||||
{"cache-control", "no-cache"},
|
||||
{"cache-control", "no-store"},
|
||||
{"cache-control", "public, max-age=31536000"},
|
||||
{"content-encoding", "br"},
|
||||
{"content-encoding", "gzip"},
|
||||
{"content-type", "application/dns-message"},
|
||||
{"content-type", "application/javascript"},
|
||||
{"content-type", "application/json"},
|
||||
{"content-type", "application/x-www-form-urlencoded"},
|
||||
{"content-type", "image/gif"},
|
||||
{"content-type", "image/jpeg"},
|
||||
{"content-type", "image/png"},
|
||||
{"content-type", "text/css"},
|
||||
{"content-type", "text/html; charset=utf-8"},
|
||||
{"content-type", "text/plain"},
|
||||
{"content-type", "text/plain;charset=utf-8"},
|
||||
{"range", "bytes=0-"},
|
||||
{"strict-transport-security", "max-age=31536000"},
|
||||
{"strict-transport-security", "max-age=31536000; includesubdomains"},
|
||||
{"strict-transport-security", "max-age=31536000; includesubdomains; preload"},
|
||||
{"vary", "accept-encoding"},
|
||||
{"vary", "origin"},
|
||||
{"x-content-type-options", "nosniff"},
|
||||
{"x-xss-protection", "1; mode=block"},
|
||||
{":status", "100"},
|
||||
{":status", "204"},
|
||||
{":status", "206"},
|
||||
{":status", "302"},
|
||||
{":status", "400"},
|
||||
{":status", "403"},
|
||||
{":status", "421"},
|
||||
{":status", "425"},
|
||||
{":status", "500"},
|
||||
{"accept-language", ""},
|
||||
{"access-control-allow-credentials", "FALSE"},
|
||||
{"access-control-allow-credentials", "TRUE"},
|
||||
{"access-control-allow-headers", "*"},
|
||||
{"access-control-allow-methods", "get"},
|
||||
{"access-control-allow-methods", "get, post, options"},
|
||||
{"access-control-allow-methods", "options"},
|
||||
{"access-control-expose-headers", "content-length"},
|
||||
{"access-control-request-headers", "content-type"},
|
||||
{"access-control-request-method", "get"},
|
||||
{"access-control-request-method", "post"},
|
||||
{"alt-svc", "clear"},
|
||||
{"authorization", ""},
|
||||
{"content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"},
|
||||
{"early-data", "1"},
|
||||
{"expect-ct", ""},
|
||||
{"forwarded", ""},
|
||||
{"if-range", ""},
|
||||
{"origin", ""},
|
||||
{"purpose", "prefetch"},
|
||||
{"server", ""},
|
||||
{"timing-allow-origin", "*"},
|
||||
{"upgrade-insecure-requests", "1"},
|
||||
{"user-agent", ""},
|
||||
{"x-forwarded-for", ""},
|
||||
{"x-frame-options", "deny"},
|
||||
{"x-frame-options", "sameorigin"},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue