1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
//! A module implementing HPACK functionality. Exposes a simple API for
//! performing the encoding and decoding of header sets, according to the
//! HPACK spec.
// Re-export the main HPACK API entry points.
pub use self::decoder::Decoder;
pub use self::encoder::Encoder;
use crate::hpack::dynamic_table::DynamicTable;
use crate::hpack::static_table::StaticTable;
use bytes::Bytes;
pub mod decoder;
mod dynamic_table;
pub mod encoder;
pub mod huffman;
mod static_table;
/// The struct represents the header table obtained by merging the static and
/// dynamic tables into a single index address space, as described in section
/// `2.3.3.` of the HPACK spec.
struct HeaderTable {
static_table: StaticTable,
dynamic_table: DynamicTable,
}
#[derive(Eq, PartialEq, Debug)]
enum HeaderValueFound {
Found,
NameOnlyFound,
}
impl HeaderTable {
/// Creates a new header table where the static part is initialized with
/// the given static table.
pub fn with_static_table(static_table: StaticTable) -> HeaderTable {
HeaderTable {
static_table: static_table,
dynamic_table: DynamicTable::new(),
}
}
/// Returns an iterator through *all* headers stored in the header table,
/// i.e. it includes both the ones found in the static table and the
/// dynamic table, in the order of their indices in the single address
/// space (first the headers in the static table, followed by headers in
/// the dynamic table).
///
/// The type yielded by the iterator is `(&[u8], &[u8])`, where the tuple
/// corresponds to the header name, value pairs in the described order.
pub fn iter(&self) -> impl Iterator<Item = (&[u8], &[u8])> {
self.static_table
.iter()
.map(|h| h)
.chain(self.dynamic_table.iter())
}
/// Adds the given header to the table. Of course, this means that the new
/// header is added to the dynamic part of the table.
///
/// If the size of the new header is larger than the current maximum table
/// size of the dynamic table, the effect will be that the dynamic table
/// gets emptied and the new header does *not* get inserted into it.
#[inline]
pub fn add_header(&mut self, name: Bytes, value: Bytes) {
self.dynamic_table.add_header(name, value);
}
#[cfg(test)]
fn add_header_for_test(&mut self, name: impl Into<Bytes>, value: impl Into<Bytes>) {
self.add_header(name.into(), value.into());
}
/// Returns a reference to the header (a `(name, value)` pair) with the
/// given index in the table.
///
/// The table is 1-indexed and constructed in such a way that the first
/// entries belong to the static table, followed by entries in the dynamic
/// table. They are merged into a single index address space, though.
///
/// This is according to the [HPACK spec, section 2.3.3.]
/// (http://http2.github.io/http2-spec/compression.html#index.address.space)
pub fn get_from_table(&self, index: usize) -> Option<(Bytes, Bytes)> {
// The IETF defined table indexing as 1-based.
// So, before starting, make sure the given index is within the proper
// bounds.
let real_index = if index > 0 { index - 1 } else { return None };
match self.static_table.get_by_index(real_index as u32) {
Ok((k, v)) => Some((Bytes::from_static(k), Bytes::from_static(v))),
Err(dynamic_index) => match self.dynamic_table.get(dynamic_index as usize) {
Some(&(ref name, ref value)) => Some((name.clone(), value.clone())),
None => None,
},
}
}
#[cfg(test)]
pub fn get_from_table_vec(&self, index: usize) -> Option<(Vec<u8>, Vec<u8>)> {
self.get_from_table(index)
.map(|(k, v)| (k.to_vec(), v.to_vec()))
}
/// Finds the given header in the header table. Tries to match both the
/// header name and value to one of the headers in the table. If no such
/// header exists, then falls back to returning one that matched only the
/// name.
///
/// # Returns
///
/// An `Option`, where `Some` corresponds to a tuple representing the index
/// of the header in the header tables (the 1-based index that HPACK uses)
/// and a `bool` indicating whether the value of the header also matched.
pub fn find_header(&self, header: (&[u8], &[u8])) -> Option<(usize, HeaderValueFound)> {
// Just does a simple scan of the entire table, searching for a header
// that matches both the name and the value of the given header.
// If no such header is found, then any one of the headers that had a
// matching name is returned, with the appropriate return flag set.
//
// The tables are so small that it is unlikely that the linear scan
// would be a major performance bottlneck. If it does prove to be,
// though, a more efficient lookup/header representation method could
// be devised.
let mut matching_name: Option<usize> = None;
for (i, h) in self.iter().enumerate() {
if header.0 == h.0 {
if header.1 == h.1 {
// Both name and value matched: returns it immediately
return Some((i + 1, HeaderValueFound::Found));
}
// If only the name was valid, we continue scanning, hoping to
// find one where both the name and value match. We remember
// this one, in case such a header isn't found after all.
matching_name = Some(i + 1);
}
}
// Finally, if there's no header with a matching name and value,
// return one that matched only the name, if that *was* found.
match matching_name {
Some(i) => Some((i, HeaderValueFound::NameOnlyFound)),
None => None,
}
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::static_table::STATIC_TABLE;
use super::HeaderTable;
use crate::hpack::static_table::StaticTable;
use crate::hpack::HeaderValueFound;
/// Tests that indexing the header table with indices that correspond to
/// entries found in the static table works.
#[test]
fn test_header_table_index_static() {
let table = HeaderTable::with_static_table(StaticTable::new());
for (index, (k, v)) in STATIC_TABLE.iter().enumerate() {
assert_eq!(
table.get_from_table(index + 1).unwrap(),
(Bytes::from(*k), Bytes::from(*v))
);
}
}
/// Tests that when the given index is out of bounds, the `HeaderTable`
/// returns a `None`
#[test]
fn test_header_table_index_out_of_bounds() {
let table = HeaderTable::with_static_table(StaticTable::new());
assert!(table.get_from_table(0).is_none());
assert!(table.get_from_table(STATIC_TABLE.len() + 1).is_none());
}
/// Tests that adding entries to the dynamic table through the
/// `HeaderTable` interface works.
#[test]
fn test_header_table_add_to_dynamic() {
let mut table = HeaderTable::with_static_table(StaticTable::new());
let header = (b"a".to_vec(), b"b".to_vec());
table.add_header_for_test(header.0.clone(), header.1.clone());
assert_eq!(table.dynamic_table.to_vec_of_vec(), vec![header]);
}
/// Tests that indexing the header table with indices that correspond to
/// entries found in the dynamic table works.
#[test]
fn test_header_table_index_dynamic() {
let mut table = HeaderTable::with_static_table(StaticTable::new());
let header = (b"a".to_vec(), b"b".to_vec());
table.add_header_for_test(header.0.clone(), header.1.clone());
assert_eq!(
table.get_from_table_vec(STATIC_TABLE.len() + 1).unwrap(),
header
);
}
/// Tests that the `iter` method of the `HeaderTable` returns an iterator
/// through *all* the headers found in the header table (static and dynamic
/// tables both included)
#[test]
fn test_header_table_iter() {
let mut table = HeaderTable::with_static_table(StaticTable::new());
let headers: [(&[u8], &[u8]); 2] = [(b"a", b"b"), (b"c", b"d")];
for header in headers.iter() {
table.add_header_for_test(header.0.to_vec(), header.1.to_vec());
}
let iterated: Vec<(&[u8], &[u8])> = table.iter().collect();
assert_eq!(iterated.len(), headers.len() + STATIC_TABLE.len());
// Part of the static table correctly iterated through
for (h1, h2) in iterated.iter().zip(STATIC_TABLE.iter()) {
assert_eq!(h1, h2);
}
// Part of the dynamic table correctly iterated through: the elements
// are in reversed order of insertion in the dynamic table.
for (h1, h2) in iterated
.iter()
.skip(STATIC_TABLE.len())
.zip(headers.iter().rev())
{
assert_eq!(h1, h2);
}
}
/// Tests that searching for an entry in the header table, which should be
/// fully in the static table (both name and value), works correctly.
#[test]
fn test_find_header_static_full() {
let table = HeaderTable::with_static_table(StaticTable::new());
for (i, h) in STATIC_TABLE.iter().enumerate() {
assert_eq!(
table.find_header(*h).unwrap(),
(i + 1, HeaderValueFound::Found)
);
}
}
/// Tests that searching for an entry in the header table, which should be
/// only partially in the static table (only the name), works correctly.
#[test]
fn test_find_header_static_partial() {
{
let table = HeaderTable::with_static_table(StaticTable::new());
let h: (&[u8], &[u8]) = (b":method", b"PUT");
if let (index, HeaderValueFound::NameOnlyFound) = table.find_header(h).unwrap() {
assert_eq!(h.0, STATIC_TABLE[index - 1].0);
// The index is the last one with the corresponding name
assert_eq!(3, index);
} else {
panic!("The header should have matched only partially");
}
}
{
let table = HeaderTable::with_static_table(StaticTable::new());
let h: (&[u8], &[u8]) = (b":status", b"333");
if let (index, HeaderValueFound::NameOnlyFound) = table.find_header(h).unwrap() {
assert_eq!(h.0, STATIC_TABLE[index - 1].0);
// The index is the last one with the corresponding name
assert_eq!(14, index);
} else {
panic!("The header should have matched only partially");
}
}
{
let table = HeaderTable::with_static_table(StaticTable::new());
let h: (&[u8], &[u8]) = (b":authority", b"example.com");
if let (index, HeaderValueFound::NameOnlyFound) = table.find_header(h).unwrap() {
assert_eq!(h.0, STATIC_TABLE[index - 1].0);
} else {
panic!("The header should have matched only partially");
}
}
{
let table = HeaderTable::with_static_table(StaticTable::new());
let h: (&[u8], &[u8]) = (b"www-authenticate", b"asdf");
if let (index, HeaderValueFound::NameOnlyFound) = table.find_header(h).unwrap() {
assert_eq!(h.0, STATIC_TABLE[index - 1].0);
} else {
panic!("The header should have matched only partially");
}
}
}
/// Tests that searching for an entry in the header table, which should be
/// fully in the dynamic table (both name and value), works correctly.
#[test]
fn test_find_header_dynamic_full() {
let mut table = HeaderTable::with_static_table(StaticTable::new());
let h: (&[u8], &[u8]) = (b":method", b"PUT");
table.add_header_for_test(h.0.to_vec(), h.1.to_vec());
if let (index, HeaderValueFound::Found) = table.find_header(h).unwrap() {
assert_eq!(index, STATIC_TABLE.len() + 1);
} else {
panic!("The header should have matched fully");
}
}
/// Tests that searching for an entry in the header table, which should be
/// only partially in the dynamic table (only the name), works correctly.
#[test]
fn test_find_header_dynamic_partial() {
let mut table = HeaderTable::with_static_table(StaticTable::new());
// First add it to the dynamic table
{
let h = (b"X-Custom-Header", b"stuff");
table.add_header_for_test(h.0.to_vec(), h.1.to_vec());
}
// Prepare a search
let h: (&[u8], &[u8]) = (b"X-Custom-Header", b"different-stuff");
// It must match only partially
if let (index, HeaderValueFound::NameOnlyFound) = table.find_header(h).unwrap() {
// The index must be the first one in the dynamic table
// segment of the header table.
assert_eq!(index, STATIC_TABLE.len() + 1);
} else {
panic!("The header should have matched only partially");
}
}
}