1 /* 2 * Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package jdk.incubator.http.internal.hpack; 24 25 import org.testng.annotations.Test; 26 import jdk.incubator.http.internal.hpack.HeaderTable.HeaderField; 27 28 import java.nio.charset.StandardCharsets; 29 import java.util.Collections; 30 import java.util.HashMap; 31 import java.util.Locale; 32 import java.util.Map; 33 import java.util.Random; 34 import java.util.regex.Matcher; 35 import java.util.regex.Pattern; 36 37 import static java.lang.String.format; 38 import static org.testng.Assert.assertEquals; 39 import static jdk.incubator.http.internal.hpack.TestHelper.assertExceptionMessageContains; 40 import static jdk.incubator.http.internal.hpack.TestHelper.assertThrows; 41 import static jdk.incubator.http.internal.hpack.TestHelper.assertVoidThrows; 42 import static jdk.incubator.http.internal.hpack.TestHelper.newRandom; 43 44 public class HeaderTableTest { 45 46 // 47 // https://tools.ietf.org/html/rfc7541#appendix-A 48 // 49 // @formatter:off 50 private static final String SPEC = 51 " | 1 | :authority | |\n" + 52 " | 2 | :method | GET |\n" + 53 " | 3 | :method | POST |\n" + 54 " | 4 | :path | / |\n" + 55 " | 5 | :path | /index.html |\n" + 56 " | 6 | :scheme | http |\n" + 57 " | 7 | :scheme | https |\n" + 58 " | 8 | :status | 200 |\n" + 59 " | 9 | :status | 204 |\n" + 60 " | 10 | :status | 206 |\n" + 61 " | 11 | :status | 304 |\n" + 62 " | 12 | :status | 400 |\n" + 63 " | 13 | :status | 404 |\n" + 64 " | 14 | :status | 500 |\n" + 65 " | 15 | accept-charset | |\n" + 66 " | 16 | accept-encoding | gzip, deflate |\n" + 67 " | 17 | accept-language | |\n" + 68 " | 18 | accept-ranges | |\n" + 69 " | 19 | accept | |\n" + 70 " | 20 | access-control-allow-origin | |\n" + 71 " | 21 | age | |\n" + 72 " | 22 | allow | |\n" + 73 " | 23 | authorization | |\n" + 74 " | 24 | cache-control | |\n" + 75 " | 25 | content-disposition | |\n" + 76 " | 26 | content-encoding | |\n" + 77 " | 27 | content-language | |\n" + 78 " | 28 | content-length | |\n" + 79 " | 29 | content-location | |\n" + 80 " | 30 | content-range | |\n" + 81 " | 31 | content-type | |\n" + 82 " | 32 | cookie | |\n" + 83 " | 33 | date | |\n" + 84 " | 34 | etag | |\n" + 85 " | 35 | expect | |\n" + 86 " | 36 | expires | |\n" + 87 " | 37 | from | |\n" + 88 " | 38 | host | |\n" + 89 " | 39 | if-match | |\n" + 90 " | 40 | if-modified-since | |\n" + 91 " | 41 | if-none-match | |\n" + 92 " | 42 | if-range | |\n" + 93 " | 43 | if-unmodified-since | |\n" + 94 " | 44 | last-modified | |\n" + 95 " | 45 | link | |\n" + 96 " | 46 | location | |\n" + 97 " | 47 | max-forwards | |\n" + 98 " | 48 | proxy-authenticate | |\n" + 99 " | 49 | proxy-authorization | |\n" + 100 " | 50 | range | |\n" + 101 " | 51 | referer | |\n" + 102 " | 52 | refresh | |\n" + 103 " | 53 | retry-after | |\n" + 104 " | 54 | server | |\n" + 105 " | 55 | set-cookie | |\n" + 106 " | 56 | strict-transport-security | |\n" + 107 " | 57 | transfer-encoding | |\n" + 108 " | 58 | user-agent | |\n" + 109 " | 59 | vary | |\n" + 110 " | 60 | via | |\n" + 111 " | 61 | www-authenticate | |\n"; 112 // @formatter:on 113 114 private static final int STATIC_TABLE_LENGTH = createStaticEntries().size(); 115 private final Random rnd = newRandom(); 116 117 @Test 118 public void staticData() { 119 HeaderTable table = new HeaderTable(0, HPACK.getLogger()); 120 Map<Integer, HeaderField> staticHeaderFields = createStaticEntries(); 121 122 Map<String, Integer> minimalIndexes = new HashMap<>(); 123 124 for (Map.Entry<Integer, HeaderField> e : staticHeaderFields.entrySet()) { 125 Integer idx = e.getKey(); 126 String hName = e.getValue().name; 127 Integer midx = minimalIndexes.get(hName); 128 if (midx == null) { 129 minimalIndexes.put(hName, idx); 130 } else { 131 minimalIndexes.put(hName, Math.min(idx, midx)); 132 } 133 } 134 135 staticHeaderFields.entrySet().forEach( 136 e -> { 137 // lookup 138 HeaderField actualHeaderField = table.get(e.getKey()); 139 HeaderField expectedHeaderField = e.getValue(); 140 assertEquals(actualHeaderField, expectedHeaderField); 141 142 // reverse lookup (name, value) 143 String hName = expectedHeaderField.name; 144 String hValue = expectedHeaderField.value; 145 int expectedIndex = e.getKey(); 146 int actualIndex = table.indexOf(hName, hValue); 147 148 assertEquals(actualIndex, expectedIndex); 149 150 // reverse lookup (name) 151 int expectedMinimalIndex = minimalIndexes.get(hName); 152 int actualMinimalIndex = table.indexOf(hName, "blah-blah"); 153 154 assertEquals(-actualMinimalIndex, expectedMinimalIndex); 155 } 156 ); 157 } 158 159 @Test 160 public void constructorSetsMaxSize() { 161 int size = rnd.nextInt(64); 162 HeaderTable t = new HeaderTable(size, HPACK.getLogger()); 163 assertEquals(t.size(), 0); 164 assertEquals(t.maxSize(), size); 165 } 166 167 @Test 168 public void negativeMaximumSize() { 169 int maxSize = -(rnd.nextInt(100) + 1); // [-100, -1] 170 IllegalArgumentException e = 171 assertVoidThrows(IllegalArgumentException.class, 172 () -> new HeaderTable(0, HPACK.getLogger()).setMaxSize(maxSize)); 173 assertExceptionMessageContains(e, "maxSize"); 174 } 175 176 @Test 177 public void zeroMaximumSize() { 178 HeaderTable table = new HeaderTable(0, HPACK.getLogger()); 179 table.setMaxSize(0); 180 assertEquals(table.maxSize(), 0); 181 } 182 183 @Test 184 public void negativeIndex() { 185 int idx = -(rnd.nextInt(256) + 1); // [-256, -1] 186 IndexOutOfBoundsException e = 187 assertVoidThrows(IndexOutOfBoundsException.class, 188 () -> new HeaderTable(0, HPACK.getLogger()).get(idx)); 189 assertExceptionMessageContains(e, "index"); 190 } 191 192 @Test 193 public void zeroIndex() { 194 IndexOutOfBoundsException e = 195 assertThrows(IndexOutOfBoundsException.class, 196 () -> new HeaderTable(0, HPACK.getLogger()).get(0)); 197 assertExceptionMessageContains(e, "index"); 198 } 199 200 @Test 201 public void length() { 202 HeaderTable table = new HeaderTable(0, HPACK.getLogger()); 203 assertEquals(table.length(), STATIC_TABLE_LENGTH); 204 } 205 206 @Test 207 public void indexOutsideStaticRange() { 208 HeaderTable table = new HeaderTable(0, HPACK.getLogger()); 209 int idx = table.length() + (rnd.nextInt(256) + 1); 210 IndexOutOfBoundsException e = 211 assertThrows(IndexOutOfBoundsException.class, 212 () -> table.get(idx)); 213 assertExceptionMessageContains(e, "index"); 214 } 215 216 @Test 217 public void entryPutAfterStaticArea() { 218 HeaderTable table = new HeaderTable(256, HPACK.getLogger()); 219 int idx = table.length() + 1; 220 assertThrows(IndexOutOfBoundsException.class, () -> table.get(idx)); 221 222 byte[] bytes = new byte[32]; 223 rnd.nextBytes(bytes); 224 String name = new String(bytes, StandardCharsets.ISO_8859_1); 225 String value = "custom-value"; 226 227 table.put(name, value); 228 HeaderField f = table.get(idx); 229 assertEquals(name, f.name); 230 assertEquals(value, f.value); 231 } 232 233 @Test 234 public void staticTableHasZeroSize() { 235 HeaderTable table = new HeaderTable(0, HPACK.getLogger()); 236 assertEquals(0, table.size()); 237 } 238 239 @Test 240 public void lowerIndexPriority() { 241 HeaderTable table = new HeaderTable(256, HPACK.getLogger()); 242 int oldLength = table.length(); 243 table.put("bender", "rodriguez"); 244 table.put("bender", "rodriguez"); 245 table.put("bender", "rodriguez"); 246 247 assertEquals(table.length(), oldLength + 3); // more like an assumption 248 int i = table.indexOf("bender", "rodriguez"); 249 assertEquals(oldLength + 1, i); 250 } 251 252 @Test 253 public void lowerIndexPriority2() { 254 HeaderTable table = new HeaderTable(256, HPACK.getLogger()); 255 int oldLength = table.length(); 256 int idx = rnd.nextInt(oldLength) + 1; 257 HeaderField f = table.get(idx); 258 table.put(f.name, f.value); 259 assertEquals(table.length(), oldLength + 1); 260 int i = table.indexOf(f.name, f.value); 261 assertEquals(idx, i); 262 } 263 264 // TODO: negative indexes check 265 // TODO: ensure full table clearance when adding huge header field 266 // TODO: ensure eviction deletes minimum needed entries, not more 267 268 @Test 269 public void fifo() { 270 // Let's add a series of header fields 271 int NUM_HEADERS = 32; 272 HeaderTable t = new HeaderTable((32 + 4) * NUM_HEADERS, HPACK.getLogger()); 273 // ^ ^ 274 // entry overhead symbols per entry (max 2x2 digits) 275 for (int i = 1; i <= NUM_HEADERS; i++) { 276 String s = String.valueOf(i); 277 t.put(s, s); 278 } 279 // They MUST appear in a FIFO order: 280 // newer entries are at lower indexes 281 // older entries are at higher indexes 282 for (int j = 1; j <= NUM_HEADERS; j++) { 283 HeaderField f = t.get(STATIC_TABLE_LENGTH + j); 284 int actualName = Integer.parseInt(f.name); 285 int expectedName = NUM_HEADERS - j + 1; 286 assertEquals(expectedName, actualName); 287 } 288 // Entries MUST be evicted in the order they were added: 289 // the newer the entry the later it is evicted 290 for (int k = 1; k <= NUM_HEADERS; k++) { 291 HeaderField f = t.evictEntry(); 292 assertEquals(String.valueOf(k), f.name); 293 } 294 } 295 296 @Test 297 public void indexOf() { 298 // Let's put a series of header fields 299 int NUM_HEADERS = 32; 300 HeaderTable t = new HeaderTable((32 + 4) * NUM_HEADERS, HPACK.getLogger()); 301 // ^ ^ 302 // entry overhead symbols per entry (max 2x2 digits) 303 for (int i = 1; i <= NUM_HEADERS; i++) { 304 String s = String.valueOf(i); 305 t.put(s, s); 306 } 307 // and verify indexOf (reverse lookup) returns correct indexes for 308 // full lookup 309 for (int j = 1; j <= NUM_HEADERS; j++) { 310 String s = String.valueOf(j); 311 int actualIndex = t.indexOf(s, s); 312 int expectedIndex = STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1; 313 assertEquals(expectedIndex, actualIndex); 314 } 315 // as well as for just a name lookup 316 for (int j = 1; j <= NUM_HEADERS; j++) { 317 String s = String.valueOf(j); 318 int actualIndex = t.indexOf(s, "blah"); 319 int expectedIndex = -(STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1); 320 assertEquals(expectedIndex, actualIndex); 321 } 322 // lookup for non-existent name returns 0 323 assertEquals(0, t.indexOf("chupacabra", "1")); 324 } 325 326 @Test 327 public void testToString() { 328 testToString0(); 329 } 330 331 @Test 332 public void testToStringDifferentLocale() { 333 Locale locale = Locale.getDefault(); 334 Locale.setDefault(Locale.FRENCH); 335 try { 336 String s = format("%.1f", 3.1); 337 assertEquals("3,1", s); // assumption of the test, otherwise the test is useless 338 testToString0(); 339 } finally { 340 Locale.setDefault(locale); 341 } 342 } 343 344 private void testToString0() { 345 HeaderTable table = new HeaderTable(0, HPACK.getLogger()); 346 { 347 int maxSize = 2048; 348 table.setMaxSize(maxSize); 349 String expected = format( 350 "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", 351 0, STATIC_TABLE_LENGTH, 0, maxSize, 0.0); 352 assertEquals(expected, table.toString()); 353 } 354 355 { 356 String name = "custom-name"; 357 String value = "custom-value"; 358 int size = 512; 359 360 table.setMaxSize(size); 361 table.put(name, value); 362 String s = table.toString(); 363 364 int used = name.length() + value.length() + 32; 365 double ratio = used * 100.0 / size; 366 367 String expected = format( 368 "dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", 369 1, STATIC_TABLE_LENGTH + 1, used, size, ratio); 370 assertEquals(expected, s); 371 } 372 373 { 374 table.setMaxSize(78); 375 table.put(":method", ""); 376 table.put(":status", ""); 377 String s = table.toString(); 378 String expected = 379 format("dynamic length: %s, full length: %s, used space: %s/%s (%.1f%%)", 380 2, STATIC_TABLE_LENGTH + 2, 78, 78, 100.0); 381 assertEquals(expected, s); 382 } 383 } 384 385 @Test 386 public void stateString() { 387 HeaderTable table = new HeaderTable(256, HPACK.getLogger()); 388 table.put("custom-key", "custom-header"); 389 // @formatter:off 390 assertEquals("[ 1] (s = 55) custom-key: custom-header\n" + 391 " Table size: 55", table.getStateString()); 392 // @formatter:on 393 } 394 395 private static Map<Integer, HeaderField> createStaticEntries() { 396 Pattern line = Pattern.compile( 397 "\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|"); 398 Matcher m = line.matcher(SPEC); 399 Map<Integer, HeaderField> result = new HashMap<>(); 400 while (m.find()) { 401 int index = Integer.parseInt(m.group("index")); 402 String name = m.group("name"); 403 String value = m.group("value"); 404 HeaderField f = new HeaderField(name, value); 405 result.put(index, f); 406 } 407 return Collections.unmodifiableMap(result); // lol 408 } 409 }