diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java index b111d3bbd43..5ed744d07c0 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/QpackEncoder.java @@ -129,8 +129,19 @@ public class QpackEncoder _handler.onInstruction(new SetCapacityInstruction(capacity)); } - public void insertCountIncrement(int increment) + public void insertCountIncrement(int increment) throws QpackException { + 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; } @@ -171,7 +182,7 @@ public class QpackEncoder _validateEncoding = validateEncoding; } - public boolean referenceEntry(Entry entry, StreamInfo streamInfo) + public boolean referenceEntry(Entry entry) { if (entry == null) return false; @@ -190,6 +201,14 @@ public class QpackEncoder return true; } + return false; + } + + public 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()) { @@ -207,7 +226,7 @@ public class QpackEncoder return false; } - public static boolean shouldIndex(HttpField httpField) + public boolean shouldIndex(HttpField httpField) { return !DO_NOT_INDEX.contains(httpField.getHeader()); } @@ -349,6 +368,38 @@ public class QpackEncoder } } + public boolean insert(HttpField field) + { + DynamicTable dynamicTable = _context.getDynamicTable(); + if (field.getValue() == null) + field = new HttpField(field.getHeader(), field.getName(), ""); + + boolean canCreateEntry = shouldIndex(field) && (Entry.getSize(field) <= dynamicTable.getSpace()); + if (!canCreateEntry) + return false; + + Entry newEntry = new Entry(field); + dynamicTable.add(newEntry); + + Entry entry = _context.get(field); + if (referenceEntry(entry)) + { + _handler.onInstruction(new DuplicateInstruction(entry.getIndex())); + return true; + } + + boolean huffman = shouldHuffmanEncode(field); + Entry nameEntry = _context.get(field.getName()); + if (referenceEntry(nameEntry)) + { + _handler.onInstruction(new IndexedNameEntryInstruction(!nameEntry.isStatic(), nameEntry.getIndex(), huffman, field.getValue())); + return true; + } + + _handler.onInstruction(new LiteralNameEntryInstruction(huffman, field.getName(), huffman, field.getValue())); + return true; + } + public static int encodeInsertCount(int reqInsertCount, int maxTableCapacity) { if (reqInsertCount == 0) diff --git a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/DynamicTable.java b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/DynamicTable.java index 966945f04f0..014694bb8cc 100644 --- a/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/DynamicTable.java +++ b/jetty-http3/http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/table/DynamicTable.java @@ -15,6 +15,7 @@ package org.eclipse.jetty.http3.qpack.table; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -22,7 +23,7 @@ import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http3.qpack.QpackContext; import org.eclipse.jetty.http3.qpack.QpackException; -public class DynamicTable +public class DynamicTable implements Iterable { public static final int FIRST_INDEX = StaticTable.STATIC_SIZE + 1; private int _capacity; @@ -189,6 +190,12 @@ public class DynamicTable return _capacity * 3 / 4; } + @Override + public Iterator iterator() + { + return _entries.iterator(); + } + @Override public String toString() { diff --git a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/EncodeDecodeTest.java b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/EncodeDecodeTest.java index fc9847e777b..29c59024371 100644 --- a/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/EncodeDecodeTest.java +++ b/jetty-http3/http3-qpack/src/test/java/org/eclipse/jetty/http3/qpack/EncodeDecodeTest.java @@ -15,10 +15,12 @@ package org.eclipse.jetty.http3.qpack; import java.nio.ByteBuffer; +import org.eclipse.jetty.http.HttpField; 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.LiteralNameEntryInstruction; import org.eclipse.jetty.http3.qpack.generator.SectionAcknowledgmentInstruction; import org.eclipse.jetty.http3.qpack.generator.SetCapacityInstruction; import org.eclipse.jetty.http3.qpack.parser.DecoderInstructionParser; @@ -79,6 +81,8 @@ public class EncodeDecodeTest assertThat(_decoderHandler.getInstruction(), instanceOf(SectionAcknowledgmentInstruction.class)); assertTrue(_decoderHandler.isEmpty()); + _decoderInstructionParser.parse(toBuffer(new SectionAcknowledgmentInstruction(streamId))); + // B.2. Dynamic Table. // Set capacity to 220. @@ -86,32 +90,33 @@ public class EncodeDecodeTest Instruction instruction = _encoderHandler.getInstruction(); assertThat(instruction, instanceOf(SetCapacityInstruction.class)); assertThat(((SetCapacityInstruction)instruction).getCapacity(), is(220)); - assertThat(BufferUtil.toHexString(toBuffer(instruction)), equalsHex("3fbd01")); + assertThat(toString(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. + streamId = 4; httpFields = HttpFields.build() .add(":authority", "www.example.com") .add(":path", "/sample/path"); - buffer = _encoder.encode(4, httpFields); + buffer = _encoder.encode(streamId, 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")); + assertThat(toString(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")); + assertThat(toString(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); + _decoder.decode(streamId, buffer); assertNull(_decoderHandler.getHttpFields()); _encoderInstructionParser.parse(toHex("c00f 7777 772e 6578 616d 706c 652e 636f 6d")); @@ -124,6 +129,17 @@ public class EncodeDecodeTest assertThat(_decoderHandler.getInstruction(), instanceOf(SectionAcknowledgmentInstruction.class)); assertTrue(_decoderHandler.isEmpty()); + + // Parse the decoder instructions on the encoder. + _decoderInstructionParser.parse(toBuffer(new InsertCountIncrementInstruction(2))); + _decoderInstructionParser.parse(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")); + _encoder.insertCountIncrement(1); } public static ByteBuffer toBuffer(Instruction instruction) @@ -134,6 +150,11 @@ public class EncodeDecodeTest 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+", "");