httlib_hpack/table/mod.rs
1//! Provides an implementation of the HPACK [indexing tables].
2//!
3//! Indexing table is a list, to which the [HPACK] saves the commonly used
4//! headers. Each entity indexes headers per connection, separately for incoming
5//! (decoding) and for outgoing (encoding) data.
6//!
7//! The numbering of entries starts with index ‘1’ and the first 61 headers are
8//! static items, keeping their position in the table. These are specific
9//! headers, provided by the HPACK specification, based on their statistical
10//! relevance, and therefore deserve their permanent position in the table.
11//!
12//! Other headers are listed in the table from position 62 onwards and are
13//! called dynamic headers. Header entries are ordered as FIFO (first-in,
14//! first-out) and duplicated entries are allowed. Dynamic headers are always
15//! inserted at index 62, which shifts all indexes of the existing custom
16//! headers one step lower. For the dynamic part of the table, we need to set a
17//! limit of how many bytes of the dynamic headers the table is allowed to
18//! store. When, while adding a header, this limit is crossed, the headers are
19//! evicted from the back of the table, so the table never exceeds the limit.
20//!
21//! This specific functioning is addressed in the HPACk specification as two
22//! separate tables, to which it refers as the static and the dynamic table.
23//! However, we are dealing with a single list, where two tables are combined
24//! into a single address space for defining index values.
25//!
26//! The illustration below shows the structure of the indexing table.
27//!
28//! ```txt
29//! <---------- Index Address Space --------->
30//! < Static Table >< Dynamic Table >
31//! +--+-------------+--++--+-------------+--+
32//! |01| ... |61||62| ... |XX|
33//! +--+-------------+--++II+-------------+DD+
34//!
35//! II = Insertion point
36//! DD = Dropping point
37//! ```
38//!
39//! Let's see how such a table is used by entities. When a client sends the
40//! request, it can indicate in the header block that a particular header and
41//! potentially also its value, should be indexed. The table for outgoing
42//! headers on the client's side would thus look something like this:
43//!
44//! | Index | Name | Value
45//! |-|-|-
46//! | 01 | :authority |
47//! | 02 | :method | GET
48//! | .. | ... | ...
49//! | 62 | name1 | value1
50//! | 63 | value2 | value2
51//!
52//! On the server’s side, when it reads the headers it would create a table that
53//! would look exactly the same. If the next client request would send the same
54//! headers, it could simply send a header block including only header indexes:
55//!
56//! ```txt
57//! 62 63 64
58//! ```
59//!
60//! The server will then look up and expand into the full headers what those
61//! indexes represent. This essentially explains the whole concept. The
62//! mechanism is innovative and highly efficient. I guess no added discussion on
63//! its effects on the performance is necessary since there are plenty of
64//! benchmarks, proving its efficacy available online.
65//!
66//! [HPACK]: https://tools.ietf.org/html/rfc7541
67//! [indexing tables]: https://tools.ietf.org/html/rfc7541#section-2.3
68
69mod dynamic;
70mod iter;
71mod r#static;
72
73pub use iter::TableIter;
74use dynamic::DynamicTable;
75use r#static::{StaticTable, STATIC_TABLE};
76
77/// A table representing a single index address space for headers where the
78/// static and the dynamic table are combined.
79#[derive(Debug)]
80pub struct Table<'a> {
81 /// THe static table with predefined headers.
82 static_table: StaticTable<'a>,
83
84 /// The dynamic table holding custom headers.
85 dynamic_table: DynamicTable,
86}
87
88impl<'a> Table<'a> {
89 /// Returns a new header table instance with the provided maximum allowed
90 /// size of the dynamic table.
91 pub fn with_dynamic_size(max_dynamic_size: u32) -> Self {
92 Self {
93 static_table: STATIC_TABLE,
94 dynamic_table: DynamicTable::with_size(max_dynamic_size),
95 }
96 }
97
98 /// Returns the total number of headers. The result includes the sum of all
99 /// entries of the static and the dynamic table combined.
100 pub fn len(&self) -> usize {
101 self.static_table.len() + self.dynamic_table.len()
102 }
103
104 /// Returns the total number of entries stored in the dynamic table.
105 pub fn dynamic_len(&self) -> usize {
106 self.dynamic_table.len()
107 }
108
109 /// Returns the total size (in octets) of all the entries stored in the
110 /// dynamic table.
111 pub fn dynamic_size(&self) -> u32 {
112 self.dynamic_table.size()
113 }
114
115 /// Returns the maximum allowed size of the dynamic table.
116 pub fn max_dynamic_size(&self) -> u32 {
117 self.dynamic_table.max_size()
118 }
119
120 /// Updates the maximum allowed size of the dynamic table.
121 pub fn update_max_dynamic_size(&mut self, size: u32) {
122 self.dynamic_table.update_max_size(size);
123 }
124
125 /// Returns an iterator through all the headers.
126 ///
127 /// It includes entries stored in the static and the dynamic table. Since
128 /// the index `0` is an invalid index, the first returned item is at index
129 /// `1`. The entries returned have indices ordered sequentially in the
130 /// single address space (first the headers in the static table, followed by
131 /// headers in the dynamic table).
132 pub fn iter(&'a self) -> TableIter<'a> {
133 TableIter{ index: 1, table: &self }
134 }
135
136 /// Finds a header by its index.
137 ///
138 /// According to the HPACK specification, the index `0` must be treated as
139 /// an invalid index number. The value for index `0` in the static table is
140 /// thus missing. The index of `0` will always return `None`.
141 pub fn get(&self, index: u32) -> Option<(&[u8], &[u8])> {
142 let index = if index == 0 {
143 return None;
144 } else {
145 index - 1
146 };
147
148 let static_len = self.static_table.len() as u32;
149 if index < static_len {
150 Some(self.static_table[index as usize])
151 } else {
152 self.dynamic_table.get(index - static_len)
153 }
154 }
155
156 /// Searches the static and the dynamic tables for the provided header.
157 ///
158 /// It tries to match both the header name and value to one of the headers
159 /// in the table. If no such header exists, then it falls back to the one
160 /// that matched only the name. The returned match contains the index of the
161 /// header in the table and a boolean indicating whether the value of the
162 /// header also matched.
163 pub fn find(&self, name: &[u8], value: &[u8]) -> Option<(usize, bool)> {
164 let mut name_match = None;
165
166 for (i, h) in self.iter().enumerate() {
167 if name == h.0 {
168 if value == h.1 {
169 return Some((i + 1, true)); // name and value matched
170 } else if name_match.is_none() {
171 name_match = Some(i + 1); // only name mached
172 }
173 }
174 }
175
176 match name_match {
177 Some(i) => Some((i, false)),
178 None => None,
179 }
180 }
181
182 /// Inserts a new header at the beginning of the dynamic table.
183 pub fn insert(&mut self, name: Vec<u8>, value: Vec<u8>) {
184 self.dynamic_table.insert(name, value);
185 }
186}
187
188impl<'a> Default for Table<'a> {
189 fn default() -> Self {
190 Self {
191 static_table: STATIC_TABLE,
192 dynamic_table: DynamicTable::default(),
193 }
194 }
195}
196
197#[cfg(test)]
198mod test {
199 use super::*;
200
201 /// A header should be added to the beginning of the dynamic table. The
202 /// first added header should have index `62`. The table should increase
203 /// in size and length after each insertion.
204 #[test]
205 fn inserts_new_headers() {
206 let mut tbl = Table::default();
207 assert_eq!(tbl.len(), 61);
208 assert_eq!(tbl.dynamic_len(), 0);
209 assert_eq!(tbl.dynamic_size(), 0);
210 tbl.insert(b"a0".to_vec(), b"b0".to_vec());
211 assert_eq!(tbl.len(), 62);
212 assert_eq!(tbl.dynamic_len(), 1);
213 assert_eq!(tbl.dynamic_size(), 36);
214 tbl.insert(b"a1".to_vec(), b"b1".to_vec());
215 assert_eq!(tbl.len(), 63);
216 assert_eq!(tbl.dynamic_len(), 2);
217 assert_eq!(tbl.dynamic_size(), 72);
218 }
219
220 /// The returned iterator should walk through all entries in the static and
221 /// the dynamic table combined.
222 #[test]
223 fn iters_through_all_headers() {
224 let mut tbl = Table::default();
225 tbl.insert(b"a0".to_vec(), b"b0".to_vec());
226 let iter = tbl.iter();
227 assert_eq!(iter.count(), 62); // 61 static + 1 dynamic
228 let last = iter.last().unwrap();
229 assert_eq!(vec![last.0, last.1], vec![b"a0", b"b0"]);
230 }
231
232 /// The table should be able to search for headers in the static or the
233 /// dynamic table. Index `0` should always return `None` because it does not
234 /// represent a valid index.
235 #[test]
236 fn find_header_by_index() {
237 let mut tbl = Table::default();
238 tbl.insert(b"a0".to_vec(), b"b0".to_vec());
239 assert_eq!(tbl.get(0), None); // invalid index
240 let h1 = tbl.get(1).unwrap();
241 assert_eq!(vec![h1.0, h1.1], vec![b":authority".to_vec(), vec![]]);
242 let h2 = tbl.get(2).unwrap();
243 assert_eq!(vec![h2.0, h2.1], vec![b":method".to_vec(), b"GET".to_vec()]);
244 let h61 = tbl.get(61).unwrap();
245 assert_eq!(vec![h61.0, h61.1], vec![b"www-authenticate".to_vec(), vec![]]);
246 let h62 = tbl.get(62).unwrap();
247 assert_eq!(vec![h62.0, h62.1], vec![b"a0", b"b0"]);
248 }
249
250 /// The table should search the static and the dynamic tables for a possible
251 /// header match. It should try to match both the header name and value to
252 /// one of the headers in the table. If no such header exists, then it
253 /// should fall back to the one that matched only the name.
254 #[test]
255 fn find_header_match() {
256 let mut tbl = Table::default();
257 tbl.insert(b"a".to_vec(), b"b".to_vec()); // index: 63
258 tbl.insert(b"a".to_vec(), b"c".to_vec()); // index: 62
259 let m = tbl.find(b":method", b"POST").unwrap(); // fully indexed
260 assert_eq!(m.0, 3); // at index 3
261 assert_eq!(m.1, true); // name and value mached
262 let m = tbl.find(b"a", b"b").unwrap(); // fully indexed
263 assert_eq!(m.0, 63); // at index 63
264 assert_eq!(m.1, true); // name and value mached
265 let m = tbl.find(b":method", b"DELETE").unwrap(); // indexed name
266 assert_eq!(m.0, 2); // at index 2
267 assert_eq!(m.1, false); // only name mached
268 let m = tbl.find(b"a", b"x").unwrap(); // indexed name
269 assert_eq!(m.0, 62); // at index 62
270 assert_eq!(m.1, false); // only name mached
271 let m = tbl.find(b"x", b"x"); // not indexed
272 assert_eq!(m, None); // not found
273 }
274}