1/* 2 * Copyright (c) 2014, 2016, 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 */ 23package jdk.incubator.http.internal.hpack; 24 25import org.testng.annotations.Test; 26import jdk.incubator.http.internal.hpack.HeaderTable.HeaderField; 27 28import java.nio.charset.StandardCharsets; 29import java.util.Collections; 30import java.util.HashMap; 31import java.util.Locale; 32import java.util.Map; 33import java.util.Random; 34import java.util.regex.Matcher; 35import java.util.regex.Pattern; 36 37import static java.lang.String.format; 38import static org.testng.Assert.assertEquals; 39import static jdk.incubator.http.internal.hpack.TestHelper.assertExceptionMessageContains; 40import static jdk.incubator.http.internal.hpack.TestHelper.assertThrows; 41import static jdk.incubator.http.internal.hpack.TestHelper.assertVoidThrows; 42import static jdk.incubator.http.internal.hpack.TestHelper.newRandom; 43 44public 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); 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); 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).setMaxSize(maxSize)); 173 assertExceptionMessageContains(e, "maxSize"); 174 } 175 176 @Test 177 public void zeroMaximumSize() { 178 HeaderTable table = new HeaderTable(0); 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 IllegalArgumentException e = 187 assertVoidThrows(IllegalArgumentException.class, 188 () -> new HeaderTable(0).get(idx)); 189 assertExceptionMessageContains(e, "index"); 190 } 191 192 @Test 193 public void zeroIndex() { 194 IllegalArgumentException e = 195 assertThrows(IllegalArgumentException.class, 196 () -> new HeaderTable(0).get(0)); 197 assertExceptionMessageContains(e, "index"); 198 } 199 200 @Test 201 public void length() { 202 HeaderTable table = new HeaderTable(0); 203 assertEquals(table.length(), STATIC_TABLE_LENGTH); 204 } 205 206 @Test 207 public void indexOutsideStaticRange() { 208 HeaderTable table = new HeaderTable(0); 209 int idx = table.length() + (rnd.nextInt(256) + 1); 210 IllegalArgumentException e = 211 assertThrows(IllegalArgumentException.class, 212 () -> table.get(idx)); 213 assertExceptionMessageContains(e, "index"); 214 } 215 216 @Test 217 public void entryPutAfterStaticArea() { 218 HeaderTable table = new HeaderTable(256); 219 int idx = table.length() + 1; 220 assertThrows(IllegalArgumentException.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); 236 assertEquals(0, table.size()); 237 } 238 239 @Test 240 public void lowerIndexPriority() { 241 HeaderTable table = new HeaderTable(256); 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); 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 HeaderTable t = new HeaderTable(Integer.MAX_VALUE); 271 // Let's add a series of header fields 272 int NUM_HEADERS = 32; 273 for (int i = 1; i <= NUM_HEADERS; i++) { 274 String s = String.valueOf(i); 275 t.put(s, s); 276 } 277 // They MUST appear in a FIFO order: 278 // newer entries are at lower indexes 279 // older entries are at higher indexes 280 for (int j = 1; j <= NUM_HEADERS; j++) { 281 HeaderField f = t.get(STATIC_TABLE_LENGTH + j); 282 int actualName = Integer.parseInt(f.name); 283 int expectedName = NUM_HEADERS - j + 1; 284 assertEquals(expectedName, actualName); 285 } 286 // Entries MUST be evicted in the order they were added: 287 // the newer the entry the later it is evicted 288 for (int k = 1; k <= NUM_HEADERS; k++) { 289 HeaderField f = t.evictEntry(); 290 assertEquals(String.valueOf(k), f.name); 291 } 292 } 293 294 @Test 295 public void indexOf() { 296 HeaderTable t = new HeaderTable(Integer.MAX_VALUE); 297 // Let's put a series of header fields 298 int NUM_HEADERS = 32; 299 for (int i = 1; i <= NUM_HEADERS; i++) { 300 String s = String.valueOf(i); 301 t.put(s, s); 302 } 303 // and verify indexOf (reverse lookup) returns correct indexes for 304 // full lookup 305 for (int j = 1; j <= NUM_HEADERS; j++) { 306 String s = String.valueOf(j); 307 int actualIndex = t.indexOf(s, s); 308 int expectedIndex = STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1; 309 assertEquals(expectedIndex, actualIndex); 310 } 311 // as well as for just a name lookup 312 for (int j = 1; j <= NUM_HEADERS; j++) { 313 String s = String.valueOf(j); 314 int actualIndex = t.indexOf(s, "blah"); 315 int expectedIndex = -(STATIC_TABLE_LENGTH + NUM_HEADERS - j + 1); 316 assertEquals(expectedIndex, actualIndex); 317 } 318 // lookup for non-existent name returns 0 319 assertEquals(0, t.indexOf("chupacabra", "1")); 320 } 321 322 @Test 323 public void testToString() { 324 testToString0(); 325 } 326 327 @Test 328 public void testToStringDifferentLocale() { 329 Locale.setDefault(Locale.FRENCH); 330 String s = format("%.1f", 3.1); 331 assertEquals("3,1", s); // assumption of the test, otherwise the test is useless 332 testToString0(); 333 } 334 335 private void testToString0() { 336 HeaderTable table = new HeaderTable(0); 337 { 338 table.setMaxSize(2048); 339 String expected = 340 format("entries: %d; used %s/%s (%.1f%%)", 0, 0, 2048, 0.0); 341 assertEquals(expected, table.toString()); 342 } 343 344 { 345 String name = "custom-name"; 346 String value = "custom-value"; 347 int size = 512; 348 349 table.setMaxSize(size); 350 table.put(name, value); 351 String s = table.toString(); 352 353 int used = name.length() + value.length() + 32; 354 double ratio = used * 100.0 / size; 355 356 String expected = 357 format("entries: 1; used %s/%s (%.1f%%)", used, size, ratio); 358 assertEquals(expected, s); 359 } 360 361 { 362 table.setMaxSize(78); 363 table.put(":method", ""); 364 table.put(":status", ""); 365 String s = table.toString(); 366 String expected = 367 format("entries: %d; used %s/%s (%.1f%%)", 2, 78, 78, 100.0); 368 assertEquals(expected, s); 369 } 370 } 371 372 @Test 373 public void stateString() { 374 HeaderTable table = new HeaderTable(256); 375 table.put("custom-key", "custom-header"); 376 // @formatter:off 377 assertEquals("[ 1] (s = 55) custom-key: custom-header\n" + 378 " Table size: 55", table.getStateString()); 379 // @formatter:on 380 } 381 382 private static Map<Integer, HeaderField> createStaticEntries() { 383 Pattern line = Pattern.compile( 384 "\\|\\s*(?<index>\\d+?)\\s*\\|\\s*(?<name>.+?)\\s*\\|\\s*(?<value>.*?)\\s*\\|"); 385 Matcher m = line.matcher(SPEC); 386 Map<Integer, HeaderField> result = new HashMap<>(); 387 while (m.find()) { 388 int index = Integer.parseInt(m.group("index")); 389 String name = m.group("name"); 390 String value = m.group("value"); 391 HeaderField f = new HeaderField(name, value); 392 result.put(index, f); 393 } 394 return Collections.unmodifiableMap(result); // lol 395 } 396} 397