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