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}