feat(core): add HPACK decoder
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package dev.relism.flash.http2.hpack;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import dev.relism.flash.http2.Http2Exception;
|
||||
import dev.relism.flash.http2.Http2Limits;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ContinuationAssemblerTest {
|
||||
@Test
|
||||
void assemblesContiguousBlock() {
|
||||
ContinuationAssembler assembler = new ContinuationAssembler(16);
|
||||
assembler.begin(3, "abc".getBytes(StandardCharsets.US_ASCII), 0, 3, false);
|
||||
assembler.continuation(3, "def".getBytes(StandardCharsets.US_ASCII), 0, 3, true);
|
||||
assertTrue(assembler.isComplete());
|
||||
assertFalse(assembler.isActive());
|
||||
assertEquals(
|
||||
"abcdef", new String(assembler.buffer(), 0, assembler.length(), StandardCharsets.US_ASCII));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInterleavingWrongStreamAndOversizedBlocks() {
|
||||
ContinuationAssembler assembler = new ContinuationAssembler(4);
|
||||
assembler.begin(1, new byte[] {1}, 0, 1, false);
|
||||
assertThrows(Http2Exception.class, () -> assembler.begin(3, new byte[0], 0, 0, true));
|
||||
assertThrows(Http2Exception.class, () -> assembler.continuation(3, new byte[0], 0, 0, true));
|
||||
assertThrows(Http2Exception.class, () -> assembler.continuation(1, new byte[4], 0, 4, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void boundsContinuationCount() {
|
||||
ContinuationAssembler assembler = new ContinuationAssembler(32);
|
||||
assembler.begin(1, new byte[0], 0, 0, false);
|
||||
for (int i = 0; i < Http2Limits.MAX_CONTINUATION_FRAMES_PER_BLOCK; i++) {
|
||||
assembler.continuation(1, new byte[0], 0, 0, false);
|
||||
}
|
||||
assertThrows(Http2Exception.class, () -> assembler.continuation(1, new byte[0], 0, 0, false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package dev.relism.flash.http2.hpack;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import dev.relism.flash.http2.Http2Exception;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HpackDecoderFuzzTest {
|
||||
private static final int CASES = 10_000_000;
|
||||
private static final HeaderSink DISCARD = (name, value, never) -> {};
|
||||
|
||||
@Test
|
||||
void tenMillionRandomBlocksOnlyProduceTypedRejections() {
|
||||
HpackDecoder decoder = new HpackDecoder(256, 1024);
|
||||
byte[] input = new byte[64];
|
||||
long state = 0x7541_9113_C0DEL;
|
||||
for (int iteration = 0; iteration < CASES; iteration++) {
|
||||
state = next(state);
|
||||
int length = (int) state & 63;
|
||||
for (int i = 0; i < length; i++) {
|
||||
state = next(state);
|
||||
input[i] = (byte) state;
|
||||
}
|
||||
try {
|
||||
decoder.decode(input, 0, length, DISCARD);
|
||||
} catch (Http2Exception | HeaderListSizeException expected) {
|
||||
// Typed protocol rejection.
|
||||
} catch (Throwable unexpected) {
|
||||
fail("unexpected failure at iteration " + iteration + ", length " + length, unexpected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long next(long value) {
|
||||
value ^= value << 13;
|
||||
value ^= value >>> 7;
|
||||
return value ^ (value << 17);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package dev.relism.flash.http2.hpack;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import dev.relism.flash.bytes.ByteWriter;
|
||||
import dev.relism.flash.http2.Http2Exception;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HpackDecoderSecurityTest {
|
||||
private static final HeaderSink DISCARD = (name, value, never) -> {};
|
||||
|
||||
@Test
|
||||
void rejectsZeroAndOutOfRangeIndices() {
|
||||
HpackDecoder decoder = new HpackDecoder();
|
||||
assertThrows(
|
||||
Http2Exception.class, () -> decoder.decode(new byte[] {(byte) 0x80}, 0, 1, DISCARD));
|
||||
assertThrows(
|
||||
Http2Exception.class, () -> decoder.decode(new byte[] {(byte) 0xff, 0}, 0, 2, DISCARD));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsLateAndOversizedTableUpdates() {
|
||||
HpackDecoder decoder = new HpackDecoder(128, 1024);
|
||||
assertThrows(
|
||||
Http2Exception.class, () -> decoder.decode(new byte[] {(byte) 0x82, 0x20}, 0, 2, DISCARD));
|
||||
|
||||
ByteWriter update = new ByteWriter(8);
|
||||
HpackIntegers.encode(update, 0x20, 5, 129);
|
||||
assertThrows(
|
||||
Http2Exception.class, () -> decoder.decode(update.array(), 0, update.length(), DISCARD));
|
||||
}
|
||||
|
||||
@Test
|
||||
void headerListLimitIsReportedOnlyAfterDynamicStateIsUpdated() {
|
||||
HpackDecoder decoder = new HpackDecoder(256, 40);
|
||||
byte[] block = java.util.HexFormat.of().parseHex("40016101624001630164");
|
||||
HeaderListSizeException error =
|
||||
assertThrows(
|
||||
HeaderListSizeException.class, () -> decoder.decode(block, 0, block.length, DISCARD));
|
||||
assertTrue(error.decodedSize() > 40);
|
||||
assertEquals(2, decoder.dynamicTable().count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedStringsAndIntegerBombsAreCompressionErrors() {
|
||||
HpackDecoder decoder = new HpackDecoder();
|
||||
assertThrows(
|
||||
Http2Exception.class, () -> decoder.decode(new byte[] {0x40, 0x01}, 0, 2, DISCARD));
|
||||
byte[] bomb = {0x3f, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x80};
|
||||
assertThrows(Http2Exception.class, () -> decoder.decode(bomb, 0, bomb.length, DISCARD));
|
||||
}
|
||||
|
||||
@Test
|
||||
void indexedDynamicNameSurvivesEvictionDuringInsertion() {
|
||||
HpackDecoder decoder = new HpackDecoder(48, 1024);
|
||||
byte[] first = java.util.HexFormat.of().parseHex("4001610d31323334353637383930313233");
|
||||
decoder.decode(first, 0, first.length, DISCARD);
|
||||
|
||||
// Dynamic index 62 supplies the name "a". Adding the new value evicts the referenced entry.
|
||||
byte[] second = java.util.HexFormat.of().parseHex("7e0d6162636465666768696a6b6c6d");
|
||||
decoder.decode(second, 0, second.length, DISCARD);
|
||||
|
||||
dev.relism.flash.bytes.PooledSlice name = new dev.relism.flash.bytes.PooledSlice();
|
||||
dev.relism.flash.bytes.PooledSlice value = new dev.relism.flash.bytes.PooledSlice();
|
||||
decoder.dynamicTable().get(1, name, value);
|
||||
assertEquals('a', name.byteAt(0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package dev.relism.flash.http2.hpack;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import dev.relism.flash.bytes.PooledSlice;
|
||||
import dev.relism.fpr.core.ByteView;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HpackDecoderTest {
|
||||
private static final HexFormat HEX = HexFormat.of();
|
||||
|
||||
private static final class CollectingSink implements HeaderSink {
|
||||
final List<String> fields = new ArrayList<>();
|
||||
final List<Boolean> neverIndexed = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void accept(ByteView name, ByteView value, boolean never) {
|
||||
fields.add(text(name) + ": " + text(value));
|
||||
neverIndexed.add(never);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendixC2IndependentRepresentations() {
|
||||
HpackDecoder decoder = new HpackDecoder();
|
||||
CollectingSink sink = decode(decoder, "400a637573746f6d2d6b65790d637573746f6d2d686561646572");
|
||||
assertEquals(List.of("custom-key: custom-header"), sink.fields);
|
||||
assertDynamic(decoder, 1, "custom-key", "custom-header", 55);
|
||||
|
||||
decoder = new HpackDecoder();
|
||||
sink = decode(decoder, "040c2f73616d706c652f70617468");
|
||||
assertEquals(List.of(":path: /sample/path"), sink.fields);
|
||||
assertEquals(0, decoder.dynamicTable().count());
|
||||
|
||||
sink = decode(decoder, "100870617373776f726406736563726574");
|
||||
assertEquals(List.of("password: secret"), sink.fields);
|
||||
assertEquals(List.of(true), sink.neverIndexed);
|
||||
assertEquals(0, decoder.dynamicTable().count());
|
||||
|
||||
sink = decode(decoder, "82");
|
||||
assertEquals(List.of(":method: GET"), sink.fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendixC3RequestsWithoutHuffman() {
|
||||
verifyRequestSequence(
|
||||
"828684410f7777772e6578616d706c652e636f6d",
|
||||
"828684be58086e6f2d6361636865",
|
||||
"828785bf400a637573746f6d2d6b65790c637573746f6d2d76616c7565");
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendixC4RequestsWithHuffman() {
|
||||
verifyRequestSequence(
|
||||
"828684418cf1e3c2e5f23a6ba0ab90f4ff",
|
||||
"828684be5886a8eb10649cbf",
|
||||
"828785bf408825a849e95ba97d7f8925a849e95bb8e8b4bf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendixC5ResponsesWithoutHuffman() {
|
||||
verifyResponseSequence(
|
||||
"4803333032580770726976617465611d4d6f6e2c203231204f637420323031332032303a31333a323120474d546e1768747470733a2f2f7777772e6578616d706c652e636f6d",
|
||||
"4803333037c1c0bf",
|
||||
"88c1611d4d6f6e2c203231204f637420323031332032303a31333a323220474d54c05a04677a69707738666f6f3d4153444a4b48514b425a584f5157454f50495541585157454f49553b206d61782d6167653d333630303b2076657273696f6e3d31");
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendixC6ResponsesWithHuffman() {
|
||||
verifyResponseSequence(
|
||||
"488264025885aec3771a4b6196d07abe941054d444a8200595040b8166e082a62d1bff6e919d29ad171863c78f0b97c8e9ae82ae43d3",
|
||||
"4883640effc1c0bf",
|
||||
"88c16196d07abe941054d444a8200595040b8166e084a62d1bffc05a839bd9ab77ad94e7821dd7f2e6c7b335dfdfcd5b3960d5af27087f3672c1ab270fb5291f9587316065c003ed4ee5b1063d5007");
|
||||
}
|
||||
|
||||
private static void verifyRequestSequence(String first, String second, String third) {
|
||||
HpackDecoder decoder = new HpackDecoder();
|
||||
assertEquals(
|
||||
List.of(":method: GET", ":scheme: http", ":path: /", ":authority: www.example.com"),
|
||||
decode(decoder, first).fields);
|
||||
assertDynamic(decoder, 1, ":authority", "www.example.com", 57);
|
||||
|
||||
assertEquals(
|
||||
List.of(
|
||||
":method: GET",
|
||||
":scheme: http",
|
||||
":path: /",
|
||||
":authority: www.example.com",
|
||||
"cache-control: no-cache"),
|
||||
decode(decoder, second).fields);
|
||||
assertDynamic(decoder, 1, "cache-control", "no-cache", 110);
|
||||
assertDynamic(decoder, 2, ":authority", "www.example.com", 110);
|
||||
|
||||
assertEquals(
|
||||
List.of(
|
||||
":method: GET",
|
||||
":scheme: https",
|
||||
":path: /index.html",
|
||||
":authority: www.example.com",
|
||||
"custom-key: custom-value"),
|
||||
decode(decoder, third).fields);
|
||||
assertDynamic(decoder, 1, "custom-key", "custom-value", 164);
|
||||
assertDynamic(decoder, 2, "cache-control", "no-cache", 164);
|
||||
assertDynamic(decoder, 3, ":authority", "www.example.com", 164);
|
||||
}
|
||||
|
||||
private static void verifyResponseSequence(String first, String second, String third) {
|
||||
HpackDecoder decoder = new HpackDecoder(256, 32_768);
|
||||
assertEquals(responseFields("302", "21"), decode(decoder, first).fields);
|
||||
assertDynamic(decoder, 1, "location", "https://www.example.com", 222);
|
||||
assertDynamic(decoder, 4, ":status", "302", 222);
|
||||
|
||||
assertEquals(responseFields("307", "21"), decode(decoder, second).fields);
|
||||
assertDynamic(decoder, 1, ":status", "307", 222);
|
||||
assertDynamic(decoder, 4, "cache-control", "private", 222);
|
||||
|
||||
List<String> expected = new ArrayList<>(responseFields("200", "22"));
|
||||
expected.add("content-encoding: gzip");
|
||||
expected.add("set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1");
|
||||
assertEquals(expected, decode(decoder, third).fields);
|
||||
assertEquals(3, decoder.dynamicTable().count());
|
||||
assertDynamic(
|
||||
decoder, 1, "set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1", 215);
|
||||
assertDynamic(decoder, 2, "content-encoding", "gzip", 215);
|
||||
assertDynamic(decoder, 3, "date", "Mon, 21 Oct 2013 20:13:22 GMT", 215);
|
||||
}
|
||||
|
||||
private static List<String> responseFields(String status, String second) {
|
||||
return List.of(
|
||||
":status: " + status,
|
||||
"cache-control: private",
|
||||
"date: Mon, 21 Oct 2013 20:13:" + second + " GMT",
|
||||
"location: https://www.example.com");
|
||||
}
|
||||
|
||||
private static CollectingSink decode(HpackDecoder decoder, String hex) {
|
||||
CollectingSink sink = new CollectingSink();
|
||||
byte[] block = HEX.parseHex(hex);
|
||||
decoder.decode(block, 0, block.length, sink);
|
||||
return sink;
|
||||
}
|
||||
|
||||
private static void assertDynamic(
|
||||
HpackDecoder decoder, int index, String expectedName, String expectedValue, int size) {
|
||||
PooledSlice name = new PooledSlice();
|
||||
PooledSlice value = new PooledSlice();
|
||||
decoder.dynamicTable().get(index, name, value);
|
||||
assertEquals(expectedName, text(name));
|
||||
assertEquals(expectedValue, text(value));
|
||||
assertEquals(size, decoder.dynamicTable().size());
|
||||
}
|
||||
|
||||
private static String text(ByteView value) {
|
||||
byte[] bytes = new byte[value.length()];
|
||||
for (int i = 0; i < bytes.length; i++) bytes[i] = value.byteAt(i);
|
||||
return new String(bytes, StandardCharsets.US_ASCII);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package dev.relism.flash.http2.hpack;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import dev.relism.flash.bytes.PooledSlice;
|
||||
import dev.relism.flash.http2.Http2Exception;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HpackDynamicTableTest {
|
||||
private static PooledSlice view(String text) {
|
||||
byte[] bytes = text.getBytes(StandardCharsets.US_ASCII);
|
||||
PooledSlice result = new PooledSlice();
|
||||
result.reset(bytes, 0, bytes.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String text(PooledSlice value) {
|
||||
return new String(value.array(), value.offset(), value.length(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
@Test
|
||||
void newestEntryHasLowestDynamicIndex() {
|
||||
HpackDynamicTable table = new HpackDynamicTable(256);
|
||||
table.add(view("a"), view("one"));
|
||||
table.add(view("b"), view("two"));
|
||||
PooledSlice name = new PooledSlice();
|
||||
PooledSlice value = new PooledSlice();
|
||||
table.get(1, name, value);
|
||||
assertEquals("b", text(name));
|
||||
assertEquals("two", text(value));
|
||||
table.get(2, name, value);
|
||||
assertEquals("a", text(name));
|
||||
}
|
||||
|
||||
@Test
|
||||
void evictsOldestEntriesByRfcSize() {
|
||||
HpackDynamicTable table = new HpackDynamicTable(70);
|
||||
table.add(view("a"), view("1")); // 34
|
||||
table.add(view("b"), view("2")); // 34
|
||||
table.add(view("c"), view("3")); // evicts a
|
||||
assertEquals(2, table.count());
|
||||
PooledSlice name = new PooledSlice();
|
||||
table.get(2, name, new PooledSlice());
|
||||
assertEquals("b", text(name));
|
||||
}
|
||||
|
||||
@Test
|
||||
void oversizedEntryClearsTableWithoutInsertion() {
|
||||
HpackDynamicTable table = new HpackDynamicTable(40);
|
||||
table.add(view("a"), view("1"));
|
||||
table.add(view("long-name"), view("long-value"));
|
||||
assertEquals(0, table.count());
|
||||
assertEquals(0, table.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sizeUpdateCannotExceedAdvertisedMaximum() {
|
||||
HpackDynamicTable table = new HpackDynamicTable(128);
|
||||
assertThrows(Http2Exception.class, () -> table.setMaximumSize(129));
|
||||
table.setMaximumSize(0);
|
||||
assertEquals(0, table.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compactionPreservesLiveEntries() {
|
||||
HpackDynamicTable table = new HpackDynamicTable(96);
|
||||
for (int i = 0; i < 30; i++) table.add(view("name" + i), view("v" + i));
|
||||
PooledSlice name = new PooledSlice();
|
||||
PooledSlice value = new PooledSlice();
|
||||
table.get(1, name, value);
|
||||
assertEquals("name29", text(name));
|
||||
assertEquals("v29", text(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package dev.relism.flash.http2.hpack;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import dev.relism.flash.bytes.PooledSlice;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HexFormat;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HpackEvictionRaceTest {
|
||||
@Test
|
||||
void perStreamCopySurvivesConcurrentDynamicTableEviction() throws Exception {
|
||||
HpackDecoder decoder = new HpackDecoder(64, 1024);
|
||||
HpackHeaderBlock stream = new HpackHeaderBlock(1024, 16);
|
||||
|
||||
byte[] first = HexFormat.of().parseHex("40046e616d650b66697273742d76616c7565");
|
||||
decoder.decode(first, 0, first.length, stream);
|
||||
assertField(stream, 0, "name", "first-value");
|
||||
|
||||
byte[] replacement =
|
||||
HexFormat.of().parseHex("400a6f746865722d6e616d650c7365636f6e642d76616c7565");
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Future<Void>[] readers = new Future[8];
|
||||
for (int reader = 0; reader < readers.length; reader++) {
|
||||
readers[reader] =
|
||||
executor.submit(
|
||||
() -> {
|
||||
start.await();
|
||||
for (int i = 0; i < 10_000; i++) {
|
||||
assertField(stream, 0, "name", "first-value");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
start.countDown();
|
||||
for (int i = 0; i < 10_000; i++) {
|
||||
decoder.decode(replacement, 0, replacement.length, (n, v, x) -> {});
|
||||
}
|
||||
for (Future<Void> reader : readers) reader.get();
|
||||
}
|
||||
|
||||
assertField(stream, 0, "name", "first-value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void directDynamicTableViewDemonstratesTheEvictionHazard() {
|
||||
HpackDynamicTable table = new HpackDynamicTable(64);
|
||||
PooledSlice firstName = view("name");
|
||||
PooledSlice firstValue = view("first-value");
|
||||
table.add(firstName, firstValue);
|
||||
|
||||
PooledSlice borrowedName = new PooledSlice();
|
||||
PooledSlice borrowedValue = new PooledSlice();
|
||||
table.get(1, borrowedName, borrowedValue);
|
||||
String before = text(borrowedValue);
|
||||
|
||||
table.add(view("other-name"), view("second-value"));
|
||||
table.add(view("other-name"), view("second-value"));
|
||||
table.add(view("other-name"), view("second-value"));
|
||||
assertNotEquals(before, text(borrowedValue));
|
||||
}
|
||||
|
||||
private static void assertField(
|
||||
HpackHeaderBlock block, int index, String expectedName, String expectedValue) {
|
||||
PooledSlice name = new PooledSlice();
|
||||
PooledSlice value = new PooledSlice();
|
||||
block.get(index, name, value);
|
||||
assertEquals(expectedName, text(name));
|
||||
assertEquals(expectedValue, text(value));
|
||||
}
|
||||
|
||||
private static PooledSlice view(String text) {
|
||||
byte[] bytes = text.getBytes(StandardCharsets.US_ASCII);
|
||||
PooledSlice view = new PooledSlice();
|
||||
view.reset(bytes, 0, bytes.length);
|
||||
return view;
|
||||
}
|
||||
|
||||
private static String text(PooledSlice value) {
|
||||
return new String(value.array(), value.offset(), value.length(), StandardCharsets.US_ASCII);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package dev.relism.flash.http2.hpack;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import dev.relism.flash.bytes.PooledSlice;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HpackStaticTableTest {
|
||||
private static PooledSlice view(String value) {
|
||||
byte[] bytes = value.getBytes(StandardCharsets.US_ASCII);
|
||||
PooledSlice view = new PooledSlice();
|
||||
view.reset(bytes, 0, bytes.length);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsAllRfcEntriesAndUsesOneBasedIndices() {
|
||||
assertEquals(61, HpackStaticTable.LENGTH);
|
||||
assertArrayEquals(":authority".getBytes(StandardCharsets.US_ASCII), HpackStaticTable.name(1));
|
||||
assertArrayEquals(
|
||||
"gzip, deflate".getBytes(StandardCharsets.US_ASCII), HpackStaticTable.value(16));
|
||||
assertArrayEquals(
|
||||
"www-authenticate".getBytes(StandardCharsets.US_ASCII), HpackStaticTable.name(61));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> HpackStaticTable.name(0));
|
||||
assertThrows(IndexOutOfBoundsException.class, () -> HpackStaticTable.value(62));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findNameReturnsLowestIndexForRepeatedNames() {
|
||||
assertEquals(2, HpackStaticTable.findName(view(":method")));
|
||||
assertEquals(8, HpackStaticTable.findName(view(":status")));
|
||||
assertEquals(0, HpackStaticTable.findName(view("missing")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findPairMatchesExactBytes() {
|
||||
assertEquals(2, HpackStaticTable.findPair(view(":method"), view("GET")));
|
||||
assertEquals(14, HpackStaticTable.findPair(view(":status"), view("500")));
|
||||
assertEquals(0, HpackStaticTable.findPair(view(":method"), view("get")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user