1 /* 2 * Copyright (C) 2013 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.okhttp.internal.framed; 18 19 import java.io.IOException; 20 import java.util.Arrays; 21 import java.util.ArrayList; 22 import java.util.List; 23 import okio.Buffer; 24 import okio.ByteString; 25 import org.junit.Before; 26 import org.junit.Test; 27 import org.junit.runner.RunWith; 28 import org.junit.runners.JUnit4; 29 30 import static okio.ByteString.decodeHex; 31 import static org.junit.Assert.assertEquals; 32 import static org.junit.Assert.fail; 33 34 @RunWith(JUnit4.class) 35 public class HpackTest { 36 headerEntries(String... elements)37 private static List<Header> headerEntries(String... elements) { 38 List<Header> result = new ArrayList<>(elements.length / 2); 39 for (int i = 0; i < elements.length; i += 2) { 40 result.add(new Header(elements[i], elements[i + 1])); 41 } 42 return result; 43 } 44 45 private final Buffer bytesIn = new Buffer(); 46 private Hpack.Reader hpackReader; 47 private Buffer bytesOut = new Buffer(); 48 private Hpack.Writer hpackWriter; 49 reset()50 @Before public void reset() { 51 hpackReader = newReader(bytesIn); 52 hpackWriter = new Hpack.Writer(bytesOut); 53 } 54 55 /** 56 * Variable-length quantity special cases strings which are longer than 127 57 * bytes. Values such as cookies can be 4KiB, and should be possible to send. 58 * 59 * <p> http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-5.2 60 */ largeHeaderValue()61 @Test public void largeHeaderValue() throws IOException { 62 char[] value = new char[4096]; 63 Arrays.fill(value, '!'); 64 List<Header> headerBlock = headerEntries("cookie", new String(value)); 65 66 hpackWriter.writeHeaders(headerBlock); 67 bytesIn.writeAll(bytesOut); 68 hpackReader.readHeaders(); 69 70 assertEquals(0, hpackReader.dynamicTableHeaderCount); 71 72 assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); 73 } 74 75 /** 76 * HPACK has a max header table size, which can be smaller than the max header message. 77 * Ensure the larger header content is not lost. 78 */ tooLargeToHPackIsStillEmitted()79 @Test public void tooLargeToHPackIsStillEmitted() throws IOException { 80 bytesIn.writeByte(0x00); // Literal indexed 81 bytesIn.writeByte(0x0a); // Literal name (len = 10) 82 bytesIn.writeUtf8("custom-key"); 83 84 bytesIn.writeByte(0x0d); // Literal value (len = 13) 85 bytesIn.writeUtf8("custom-header"); 86 87 hpackReader.headerTableSizeSetting(1); 88 hpackReader.readHeaders(); 89 90 assertEquals(0, hpackReader.dynamicTableHeaderCount); 91 92 assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); 93 } 94 setMaxDynamicTableToZeroDoesNotClearHeaderList()95 @Test public void setMaxDynamicTableToZeroDoesNotClearHeaderList() throws IOException { 96 bytesIn.writeByte(0x40); // Literal indexed 97 bytesIn.writeByte(0x0a); // Literal name (len = 10) 98 bytesIn.writeUtf8("custom-key"); 99 100 bytesIn.writeByte(0x0d); // Literal value (len = 13) 101 bytesIn.writeUtf8("custom-header"); 102 103 bytesIn.writeByte(0x20); // Set max table size to 0 104 105 hpackReader.readHeaders(); 106 107 assertEquals(0, hpackReader.maxDynamicTableByteCount()); 108 assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); 109 } 110 111 /** Oldest entries are evicted to support newer ones. */ testEviction()112 @Test public void testEviction() throws IOException { 113 bytesIn.writeByte(0x40); // Literal indexed 114 bytesIn.writeByte(0x0a); // Literal name (len = 10) 115 bytesIn.writeUtf8("custom-foo"); 116 117 bytesIn.writeByte(0x0d); // Literal value (len = 13) 118 bytesIn.writeUtf8("custom-header"); 119 120 bytesIn.writeByte(0x40); // Literal indexed 121 bytesIn.writeByte(0x0a); // Literal name (len = 10) 122 bytesIn.writeUtf8("custom-bar"); 123 124 bytesIn.writeByte(0x0d); // Literal value (len = 13) 125 bytesIn.writeUtf8("custom-header"); 126 127 bytesIn.writeByte(0x40); // Literal indexed 128 bytesIn.writeByte(0x0a); // Literal name (len = 10) 129 bytesIn.writeUtf8("custom-baz"); 130 131 bytesIn.writeByte(0x0d); // Literal value (len = 13) 132 bytesIn.writeUtf8("custom-header"); 133 134 // Set to only support 110 bytes (enough for 2 headers). 135 hpackReader.headerTableSizeSetting(110); 136 hpackReader.readHeaders(); 137 138 assertEquals(2, hpackReader.dynamicTableHeaderCount); 139 140 Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; 141 checkEntry(entry, "custom-bar", "custom-header", 55); 142 143 entry = hpackReader.dynamicTable[headerTableLength() - 2]; 144 checkEntry(entry, "custom-baz", "custom-header", 55); 145 146 // Once a header field is decoded and added to the reconstructed header 147 // list, it cannot be removed from it. Hence, foo is here. 148 assertEquals( 149 headerEntries( 150 "custom-foo", "custom-header", 151 "custom-bar", "custom-header", 152 "custom-baz", "custom-header"), 153 hpackReader.getAndResetHeaderList()); 154 155 // Simulate receiving a small settings frame, that implies eviction. 156 hpackReader.headerTableSizeSetting(55); 157 assertEquals(1, hpackReader.dynamicTableHeaderCount); 158 } 159 160 /** Header table backing array is initially 8 long, let's ensure it grows. */ dynamicallyGrowsBeyond64Entries()161 @Test public void dynamicallyGrowsBeyond64Entries() throws IOException { 162 for (int i = 0; i < 256; i++) { 163 bytesIn.writeByte(0x40); // Literal indexed 164 bytesIn.writeByte(0x0a); // Literal name (len = 10) 165 bytesIn.writeUtf8("custom-foo"); 166 167 bytesIn.writeByte(0x0d); // Literal value (len = 13) 168 bytesIn.writeUtf8("custom-header"); 169 } 170 171 hpackReader.headerTableSizeSetting(16384); // Lots of headers need more room! 172 hpackReader.readHeaders(); 173 174 assertEquals(256, hpackReader.dynamicTableHeaderCount); 175 } 176 huffmanDecodingSupported()177 @Test public void huffmanDecodingSupported() throws IOException { 178 bytesIn.writeByte(0x44); // == Literal indexed == 179 // Indexed name (idx = 4) -> :path 180 bytesIn.writeByte(0x8c); // Literal value Huffman encoded 12 bytes 181 // decodes to www.example.com which is length 15 182 bytesIn.write(decodeHex("f1e3c2e5f23a6ba0ab90f4ff")); 183 184 hpackReader.readHeaders(); 185 186 assertEquals(1, hpackReader.dynamicTableHeaderCount); 187 assertEquals(52, hpackReader.dynamicTableByteCount); 188 189 Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; 190 checkEntry(entry, ":path", "www.example.com", 52); 191 } 192 193 /** 194 * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.1 195 */ readLiteralHeaderFieldWithIndexing()196 @Test public void readLiteralHeaderFieldWithIndexing() throws IOException { 197 bytesIn.writeByte(0x40); // Literal indexed 198 bytesIn.writeByte(0x0a); // Literal name (len = 10) 199 bytesIn.writeUtf8("custom-key"); 200 201 bytesIn.writeByte(0x0d); // Literal value (len = 13) 202 bytesIn.writeUtf8("custom-header"); 203 204 hpackReader.readHeaders(); 205 206 assertEquals(1, hpackReader.dynamicTableHeaderCount); 207 assertEquals(55, hpackReader.dynamicTableByteCount); 208 209 Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; 210 checkEntry(entry, "custom-key", "custom-header", 55); 211 212 assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); 213 } 214 215 /** 216 * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.2 217 */ literalHeaderFieldWithoutIndexingIndexedName()218 @Test public void literalHeaderFieldWithoutIndexingIndexedName() throws IOException { 219 List<Header> headerBlock = headerEntries(":path", "/sample/path"); 220 221 bytesIn.writeByte(0x04); // == Literal not indexed == 222 // Indexed name (idx = 4) -> :path 223 bytesIn.writeByte(0x0c); // Literal value (len = 12) 224 bytesIn.writeUtf8("/sample/path"); 225 226 hpackWriter.writeHeaders(headerBlock); 227 assertEquals(bytesIn, bytesOut); 228 229 hpackReader.readHeaders(); 230 231 assertEquals(0, hpackReader.dynamicTableHeaderCount); 232 233 assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); 234 } 235 literalHeaderFieldWithoutIndexingNewName()236 @Test public void literalHeaderFieldWithoutIndexingNewName() throws IOException { 237 List<Header> headerBlock = headerEntries("custom-key", "custom-header"); 238 239 bytesIn.writeByte(0x00); // Not indexed 240 bytesIn.writeByte(0x0a); // Literal name (len = 10) 241 bytesIn.writeUtf8("custom-key"); 242 243 bytesIn.writeByte(0x0d); // Literal value (len = 13) 244 bytesIn.writeUtf8("custom-header"); 245 246 hpackWriter.writeHeaders(headerBlock); 247 assertEquals(bytesIn, bytesOut); 248 249 hpackReader.readHeaders(); 250 251 assertEquals(0, hpackReader.dynamicTableHeaderCount); 252 253 assertEquals(headerBlock, hpackReader.getAndResetHeaderList()); 254 } 255 literalHeaderFieldNeverIndexedIndexedName()256 @Test public void literalHeaderFieldNeverIndexedIndexedName() throws IOException { 257 bytesIn.writeByte(0x14); // == Literal never indexed == 258 // Indexed name (idx = 4) -> :path 259 bytesIn.writeByte(0x0c); // Literal value (len = 12) 260 bytesIn.writeUtf8("/sample/path"); 261 262 hpackReader.readHeaders(); 263 264 assertEquals(0, hpackReader.dynamicTableHeaderCount); 265 266 assertEquals(headerEntries(":path", "/sample/path"), hpackReader.getAndResetHeaderList()); 267 } 268 literalHeaderFieldNeverIndexedNewName()269 @Test public void literalHeaderFieldNeverIndexedNewName() throws IOException { 270 bytesIn.writeByte(0x10); // Never indexed 271 bytesIn.writeByte(0x0a); // Literal name (len = 10) 272 bytesIn.writeUtf8("custom-key"); 273 274 bytesIn.writeByte(0x0d); // Literal value (len = 13) 275 bytesIn.writeUtf8("custom-header"); 276 277 hpackReader.readHeaders(); 278 279 assertEquals(0, hpackReader.dynamicTableHeaderCount); 280 281 assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList()); 282 } 283 staticHeaderIsNotCopiedIntoTheIndexedTable()284 @Test public void staticHeaderIsNotCopiedIntoTheIndexedTable() throws IOException { 285 bytesIn.writeByte(0x82); // == Indexed - Add == 286 // idx = 2 -> :method: GET 287 288 hpackReader.readHeaders(); 289 290 assertEquals(0, hpackReader.dynamicTableHeaderCount); 291 assertEquals(0, hpackReader.dynamicTableByteCount); 292 293 assertEquals(null, hpackReader.dynamicTable[headerTableLength() - 1]); 294 295 assertEquals(headerEntries(":method", "GET"), hpackReader.getAndResetHeaderList()); 296 } 297 298 // Example taken from twitter/hpack DecoderTest.testUnusedIndex readIndexedHeaderFieldIndex0()299 @Test public void readIndexedHeaderFieldIndex0() throws IOException { 300 bytesIn.writeByte(0x80); // == Indexed - Add idx = 0 301 302 try { 303 hpackReader.readHeaders(); 304 fail(); 305 } catch (IOException e) { 306 assertEquals("index == 0", e.getMessage()); 307 } 308 } 309 310 // Example taken from twitter/hpack DecoderTest.testIllegalIndex readIndexedHeaderFieldTooLargeIndex()311 @Test public void readIndexedHeaderFieldTooLargeIndex() throws IOException { 312 bytesIn.writeShort(0xff00); // == Indexed - Add idx = 127 313 314 try { 315 hpackReader.readHeaders(); 316 fail(); 317 } catch (IOException e) { 318 assertEquals("Header index too large 127", e.getMessage()); 319 } 320 } 321 322 // Example taken from twitter/hpack DecoderTest.testInsidiousIndex readIndexedHeaderFieldInsidiousIndex()323 @Test public void readIndexedHeaderFieldInsidiousIndex() throws IOException { 324 bytesIn.writeByte(0xff); // == Indexed - Add == 325 bytesIn.write(decodeHex("8080808008")); // idx = -2147483521 326 327 try { 328 hpackReader.readHeaders(); 329 fail(); 330 } catch (IOException e) { 331 assertEquals("Header index too large -2147483521", e.getMessage()); 332 } 333 } 334 335 // Example taken from twitter/hpack DecoderTest.testHeaderTableSizeUpdate minMaxHeaderTableSize()336 @Test public void minMaxHeaderTableSize() throws IOException { 337 bytesIn.writeByte(0x20); 338 hpackReader.readHeaders(); 339 340 assertEquals(0, hpackReader.maxDynamicTableByteCount()); 341 342 bytesIn.writeByte(0x3f); // encode size 4096 343 bytesIn.writeByte(0xe1); 344 bytesIn.writeByte(0x1f); 345 hpackReader.readHeaders(); 346 347 assertEquals(4096, hpackReader.maxDynamicTableByteCount()); 348 } 349 350 // Example taken from twitter/hpack DecoderTest.testIllegalHeaderTableSizeUpdate cannotSetTableSizeLargerThanSettingsValue()351 @Test public void cannotSetTableSizeLargerThanSettingsValue() throws IOException { 352 bytesIn.writeByte(0x3f); // encode size 4097 353 bytesIn.writeByte(0xe2); 354 bytesIn.writeByte(0x1f); 355 356 try { 357 hpackReader.readHeaders(); 358 fail(); 359 } catch (IOException e) { 360 assertEquals("Invalid dynamic table size update 4097", e.getMessage()); 361 } 362 } 363 364 // Example taken from twitter/hpack DecoderTest.testInsidiousMaxHeaderSize readHeaderTableStateChangeInsidiousMaxHeaderByteCount()365 @Test public void readHeaderTableStateChangeInsidiousMaxHeaderByteCount() throws IOException { 366 bytesIn.writeByte(0x3f); 367 bytesIn.write(decodeHex("e1ffffff07")); // count = -2147483648 368 369 try { 370 hpackReader.readHeaders(); 371 fail(); 372 } catch (IOException e) { 373 assertEquals("Invalid dynamic table size update -2147483648", e.getMessage()); 374 } 375 } 376 377 /** 378 * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.4 379 */ readIndexedHeaderFieldFromStaticTableWithoutBuffering()380 @Test public void readIndexedHeaderFieldFromStaticTableWithoutBuffering() throws IOException { 381 bytesIn.writeByte(0x82); // == Indexed - Add == 382 // idx = 2 -> :method: GET 383 384 hpackReader.headerTableSizeSetting(0); // SETTINGS_HEADER_TABLE_SIZE == 0 385 hpackReader.readHeaders(); 386 387 // Not buffered in header table. 388 assertEquals(0, hpackReader.dynamicTableHeaderCount); 389 390 assertEquals(headerEntries(":method", "GET"), hpackReader.getAndResetHeaderList()); 391 } 392 393 /** 394 * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2 395 */ readRequestExamplesWithoutHuffman()396 @Test public void readRequestExamplesWithoutHuffman() throws IOException { 397 firstRequestWithoutHuffman(); 398 hpackReader.readHeaders(); 399 checkReadFirstRequestWithoutHuffman(); 400 401 secondRequestWithoutHuffman(); 402 hpackReader.readHeaders(); 403 checkReadSecondRequestWithoutHuffman(); 404 405 thirdRequestWithoutHuffman(); 406 hpackReader.readHeaders(); 407 checkReadThirdRequestWithoutHuffman(); 408 } 409 firstRequestWithoutHuffman()410 private void firstRequestWithoutHuffman() { 411 bytesIn.writeByte(0x82); // == Indexed - Add == 412 // idx = 2 -> :method: GET 413 bytesIn.writeByte(0x86); // == Indexed - Add == 414 // idx = 7 -> :scheme: http 415 bytesIn.writeByte(0x84); // == Indexed - Add == 416 // idx = 6 -> :path: / 417 bytesIn.writeByte(0x41); // == Literal indexed == 418 // Indexed name (idx = 4) -> :authority 419 bytesIn.writeByte(0x0f); // Literal value (len = 15) 420 bytesIn.writeUtf8("www.example.com"); 421 } 422 checkReadFirstRequestWithoutHuffman()423 private void checkReadFirstRequestWithoutHuffman() { 424 assertEquals(1, hpackReader.dynamicTableHeaderCount); 425 426 // [ 1] (s = 57) :authority: www.example.com 427 Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; 428 checkEntry(entry, ":authority", "www.example.com", 57); 429 430 // Table size: 57 431 assertEquals(57, hpackReader.dynamicTableByteCount); 432 433 // Decoded header list: 434 assertEquals(headerEntries( 435 ":method", "GET", 436 ":scheme", "http", 437 ":path", "/", 438 ":authority", "www.example.com"), hpackReader.getAndResetHeaderList()); 439 } 440 secondRequestWithoutHuffman()441 private void secondRequestWithoutHuffman() { 442 bytesIn.writeByte(0x82); // == Indexed - Add == 443 // idx = 2 -> :method: GET 444 bytesIn.writeByte(0x86); // == Indexed - Add == 445 // idx = 7 -> :scheme: http 446 bytesIn.writeByte(0x84); // == Indexed - Add == 447 // idx = 6 -> :path: / 448 bytesIn.writeByte(0xbe); // == Indexed - Add == 449 // Indexed name (idx = 62) -> :authority: www.example.com 450 bytesIn.writeByte(0x58); // == Literal indexed == 451 // Indexed name (idx = 24) -> cache-control 452 bytesIn.writeByte(0x08); // Literal value (len = 8) 453 bytesIn.writeUtf8("no-cache"); 454 } 455 checkReadSecondRequestWithoutHuffman()456 private void checkReadSecondRequestWithoutHuffman() { 457 assertEquals(2, hpackReader.dynamicTableHeaderCount); 458 459 // [ 1] (s = 53) cache-control: no-cache 460 Header entry = hpackReader.dynamicTable[headerTableLength() - 2]; 461 checkEntry(entry, "cache-control", "no-cache", 53); 462 463 // [ 2] (s = 57) :authority: www.example.com 464 entry = hpackReader.dynamicTable[headerTableLength() - 1]; 465 checkEntry(entry, ":authority", "www.example.com", 57); 466 467 // Table size: 110 468 assertEquals(110, hpackReader.dynamicTableByteCount); 469 470 // Decoded header list: 471 assertEquals(headerEntries( 472 ":method", "GET", 473 ":scheme", "http", 474 ":path", "/", 475 ":authority", "www.example.com", 476 "cache-control", "no-cache"), hpackReader.getAndResetHeaderList()); 477 } 478 thirdRequestWithoutHuffman()479 private void thirdRequestWithoutHuffman() { 480 bytesIn.writeByte(0x82); // == Indexed - Add == 481 // idx = 2 -> :method: GET 482 bytesIn.writeByte(0x87); // == Indexed - Add == 483 // idx = 7 -> :scheme: http 484 bytesIn.writeByte(0x85); // == Indexed - Add == 485 // idx = 5 -> :path: /index.html 486 bytesIn.writeByte(0xbf); // == Indexed - Add == 487 // Indexed name (idx = 63) -> :authority: www.example.com 488 bytesIn.writeByte(0x40); // Literal indexed 489 bytesIn.writeByte(0x0a); // Literal name (len = 10) 490 bytesIn.writeUtf8("custom-key"); 491 bytesIn.writeByte(0x0c); // Literal value (len = 12) 492 bytesIn.writeUtf8("custom-value"); 493 } 494 checkReadThirdRequestWithoutHuffman()495 private void checkReadThirdRequestWithoutHuffman() { 496 assertEquals(3, hpackReader.dynamicTableHeaderCount); 497 498 // [ 1] (s = 54) custom-key: custom-value 499 Header entry = hpackReader.dynamicTable[headerTableLength() - 3]; 500 checkEntry(entry, "custom-key", "custom-value", 54); 501 502 // [ 2] (s = 53) cache-control: no-cache 503 entry = hpackReader.dynamicTable[headerTableLength() - 2]; 504 checkEntry(entry, "cache-control", "no-cache", 53); 505 506 // [ 3] (s = 57) :authority: www.example.com 507 entry = hpackReader.dynamicTable[headerTableLength() - 1]; 508 checkEntry(entry, ":authority", "www.example.com", 57); 509 510 // Table size: 164 511 assertEquals(164, hpackReader.dynamicTableByteCount); 512 513 // Decoded header list: 514 assertEquals(headerEntries( 515 ":method", "GET", 516 ":scheme", "https", 517 ":path", "/index.html", 518 ":authority", "www.example.com", 519 "custom-key", "custom-value"), hpackReader.getAndResetHeaderList()); 520 } 521 522 /** 523 * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.4 524 */ readRequestExamplesWithHuffman()525 @Test public void readRequestExamplesWithHuffman() throws IOException { 526 firstRequestWithHuffman(); 527 hpackReader.readHeaders(); 528 checkReadFirstRequestWithHuffman(); 529 530 secondRequestWithHuffman(); 531 hpackReader.readHeaders(); 532 checkReadSecondRequestWithHuffman(); 533 534 thirdRequestWithHuffman(); 535 hpackReader.readHeaders(); 536 checkReadThirdRequestWithHuffman(); 537 } 538 firstRequestWithHuffman()539 private void firstRequestWithHuffman() { 540 bytesIn.writeByte(0x82); // == Indexed - Add == 541 // idx = 2 -> :method: GET 542 bytesIn.writeByte(0x86); // == Indexed - Add == 543 // idx = 6 -> :scheme: http 544 bytesIn.writeByte(0x84); // == Indexed - Add == 545 // idx = 4 -> :path: / 546 bytesIn.writeByte(0x41); // == Literal indexed == 547 // Indexed name (idx = 1) -> :authority 548 bytesIn.writeByte(0x8c); // Literal value Huffman encoded 12 bytes 549 // decodes to www.example.com which is length 15 550 bytesIn.write(decodeHex("f1e3c2e5f23a6ba0ab90f4ff")); 551 } 552 checkReadFirstRequestWithHuffman()553 private void checkReadFirstRequestWithHuffman() { 554 assertEquals(1, hpackReader.dynamicTableHeaderCount); 555 556 // [ 1] (s = 57) :authority: www.example.com 557 Header entry = hpackReader.dynamicTable[headerTableLength() - 1]; 558 checkEntry(entry, ":authority", "www.example.com", 57); 559 560 // Table size: 57 561 assertEquals(57, hpackReader.dynamicTableByteCount); 562 563 // Decoded header list: 564 assertEquals(headerEntries( 565 ":method", "GET", 566 ":scheme", "http", 567 ":path", "/", 568 ":authority", "www.example.com"), hpackReader.getAndResetHeaderList()); 569 } 570 secondRequestWithHuffman()571 private void secondRequestWithHuffman() { 572 bytesIn.writeByte(0x82); // == Indexed - Add == 573 // idx = 2 -> :method: GET 574 bytesIn.writeByte(0x86); // == Indexed - Add == 575 // idx = 6 -> :scheme: http 576 bytesIn.writeByte(0x84); // == Indexed - Add == 577 // idx = 4 -> :path: / 578 bytesIn.writeByte(0xbe); // == Indexed - Add == 579 // idx = 62 -> :authority: www.example.com 580 bytesIn.writeByte(0x58); // == Literal indexed == 581 // Indexed name (idx = 24) -> cache-control 582 bytesIn.writeByte(0x86); // Literal value Huffman encoded 6 bytes 583 // decodes to no-cache which is length 8 584 bytesIn.write(decodeHex("a8eb10649cbf")); 585 } 586 checkReadSecondRequestWithHuffman()587 private void checkReadSecondRequestWithHuffman() { 588 assertEquals(2, hpackReader.dynamicTableHeaderCount); 589 590 // [ 1] (s = 53) cache-control: no-cache 591 Header entry = hpackReader.dynamicTable[headerTableLength() - 2]; 592 checkEntry(entry, "cache-control", "no-cache", 53); 593 594 // [ 2] (s = 57) :authority: www.example.com 595 entry = hpackReader.dynamicTable[headerTableLength() - 1]; 596 checkEntry(entry, ":authority", "www.example.com", 57); 597 598 // Table size: 110 599 assertEquals(110, hpackReader.dynamicTableByteCount); 600 601 // Decoded header list: 602 assertEquals(headerEntries( 603 ":method", "GET", 604 ":scheme", "http", 605 ":path", "/", 606 ":authority", "www.example.com", 607 "cache-control", "no-cache"), hpackReader.getAndResetHeaderList()); 608 } 609 thirdRequestWithHuffman()610 private void thirdRequestWithHuffman() { 611 bytesIn.writeByte(0x82); // == Indexed - Add == 612 // idx = 2 -> :method: GET 613 bytesIn.writeByte(0x87); // == Indexed - Add == 614 // idx = 7 -> :scheme: https 615 bytesIn.writeByte(0x85); // == Indexed - Add == 616 // idx = 5 -> :path: /index.html 617 bytesIn.writeByte(0xbf); // == Indexed - Add == 618 // idx = 63 -> :authority: www.example.com 619 bytesIn.writeByte(0x40); // Literal indexed 620 bytesIn.writeByte(0x88); // Literal name Huffman encoded 8 bytes 621 // decodes to custom-key which is length 10 622 bytesIn.write(decodeHex("25a849e95ba97d7f")); 623 bytesIn.writeByte(0x89); // Literal value Huffman encoded 9 bytes 624 // decodes to custom-value which is length 12 625 bytesIn.write(decodeHex("25a849e95bb8e8b4bf")); 626 } 627 checkReadThirdRequestWithHuffman()628 private void checkReadThirdRequestWithHuffman() { 629 assertEquals(3, hpackReader.dynamicTableHeaderCount); 630 631 // [ 1] (s = 54) custom-key: custom-value 632 Header entry = hpackReader.dynamicTable[headerTableLength() - 3]; 633 checkEntry(entry, "custom-key", "custom-value", 54); 634 635 // [ 2] (s = 53) cache-control: no-cache 636 entry = hpackReader.dynamicTable[headerTableLength() - 2]; 637 checkEntry(entry, "cache-control", "no-cache", 53); 638 639 // [ 3] (s = 57) :authority: www.example.com 640 entry = hpackReader.dynamicTable[headerTableLength() - 1]; 641 checkEntry(entry, ":authority", "www.example.com", 57); 642 643 // Table size: 164 644 assertEquals(164, hpackReader.dynamicTableByteCount); 645 646 // Decoded header list: 647 assertEquals(headerEntries( 648 ":method", "GET", 649 ":scheme", "https", 650 ":path", "/index.html", 651 ":authority", "www.example.com", 652 "custom-key", "custom-value"), hpackReader.getAndResetHeaderList()); 653 } 654 readSingleByteInt()655 @Test public void readSingleByteInt() throws IOException { 656 assertEquals(10, newReader(byteStream()).readInt(10, 31)); 657 assertEquals(10, newReader(byteStream()).readInt(0xe0 | 10, 31)); 658 } 659 readMultibyteInt()660 @Test public void readMultibyteInt() throws IOException { 661 assertEquals(1337, newReader(byteStream(154, 10)).readInt(31, 31)); 662 } 663 writeSingleByteInt()664 @Test public void writeSingleByteInt() throws IOException { 665 hpackWriter.writeInt(10, 31, 0); 666 assertBytes(10); 667 hpackWriter.writeInt(10, 31, 0xe0); 668 assertBytes(0xe0 | 10); 669 } 670 writeMultibyteInt()671 @Test public void writeMultibyteInt() throws IOException { 672 hpackWriter.writeInt(1337, 31, 0); 673 assertBytes(31, 154, 10); 674 hpackWriter.writeInt(1337, 31, 0xe0); 675 assertBytes(0xe0 | 31, 154, 10); 676 } 677 max31BitValue()678 @Test public void max31BitValue() throws IOException { 679 hpackWriter.writeInt(0x7fffffff, 31, 0); 680 assertBytes(31, 224, 255, 255, 255, 7); 681 assertEquals(0x7fffffff, 682 newReader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31)); 683 } 684 prefixMask()685 @Test public void prefixMask() throws IOException { 686 hpackWriter.writeInt(31, 31, 0); 687 assertBytes(31, 0); 688 assertEquals(31, newReader(byteStream(0)).readInt(31, 31)); 689 } 690 prefixMaskMinusOne()691 @Test public void prefixMaskMinusOne() throws IOException { 692 hpackWriter.writeInt(30, 31, 0); 693 assertBytes(30); 694 assertEquals(31, newReader(byteStream(0)).readInt(31, 31)); 695 } 696 zero()697 @Test public void zero() throws IOException { 698 hpackWriter.writeInt(0, 31, 0); 699 assertBytes(0); 700 assertEquals(0, newReader(byteStream()).readInt(0, 31)); 701 } 702 lowercaseHeaderNameBeforeEmit()703 @Test public void lowercaseHeaderNameBeforeEmit() throws IOException { 704 hpackWriter.writeHeaders(Arrays.asList(new Header("FoO", "BaR"))); 705 assertBytes(0, 3, 'f', 'o', 'o', 3, 'B', 'a', 'R'); 706 } 707 mixedCaseHeaderNameIsMalformed()708 @Test public void mixedCaseHeaderNameIsMalformed() throws IOException { 709 try { 710 newReader(byteStream(0, 3, 'F', 'o', 'o', 3, 'B', 'a', 'R')).readHeaders(); 711 fail(); 712 } catch (IOException e) { 713 assertEquals("PROTOCOL_ERROR response malformed: mixed case name: Foo", e.getMessage()); 714 } 715 } 716 emptyHeaderName()717 @Test public void emptyHeaderName() throws IOException { 718 hpackWriter.writeByteString(ByteString.encodeUtf8("")); 719 assertBytes(0); 720 assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString()); 721 } 722 newReader(Buffer source)723 private Hpack.Reader newReader(Buffer source) { 724 return new Hpack.Reader(4096, source); 725 } 726 byteStream(int... bytes)727 private Buffer byteStream(int... bytes) { 728 return new Buffer().write(intArrayToByteArray(bytes)); 729 } 730 checkEntry(Header entry, String name, String value, int size)731 private void checkEntry(Header entry, String name, String value, int size) { 732 assertEquals(name, entry.name.utf8()); 733 assertEquals(value, entry.value.utf8()); 734 assertEquals(size, entry.hpackSize); 735 } 736 assertBytes(int... bytes)737 private void assertBytes(int... bytes) throws IOException { 738 ByteString expected = intArrayToByteArray(bytes); 739 ByteString actual = bytesOut.readByteString(); 740 assertEquals(expected, actual); 741 } 742 intArrayToByteArray(int[] bytes)743 private ByteString intArrayToByteArray(int[] bytes) { 744 byte[] data = new byte[bytes.length]; 745 for (int i = 0; i < bytes.length; i++) { 746 data[i] = (byte) bytes[i]; 747 } 748 return ByteString.of(data); 749 } 750 headerTableLength()751 private int headerTableLength() { 752 return hpackReader.dynamicTable.length; 753 } 754 } 755