1use alloc::collections::VecDeque;
11use alloc::string::String;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct StaticTableEntry {
16 pub name: &'static str,
18 pub value: &'static str,
20}
21
22pub const STATIC_TABLE: [StaticTableEntry; 61] = [
24 StaticTableEntry {
25 name: ":authority",
26 value: "",
27 },
28 StaticTableEntry {
29 name: ":method",
30 value: "GET",
31 },
32 StaticTableEntry {
33 name: ":method",
34 value: "POST",
35 },
36 StaticTableEntry {
37 name: ":path",
38 value: "/",
39 },
40 StaticTableEntry {
41 name: ":path",
42 value: "/index.html",
43 },
44 StaticTableEntry {
45 name: ":scheme",
46 value: "http",
47 },
48 StaticTableEntry {
49 name: ":scheme",
50 value: "https",
51 },
52 StaticTableEntry {
53 name: ":status",
54 value: "200",
55 },
56 StaticTableEntry {
57 name: ":status",
58 value: "204",
59 },
60 StaticTableEntry {
61 name: ":status",
62 value: "206",
63 },
64 StaticTableEntry {
65 name: ":status",
66 value: "304",
67 },
68 StaticTableEntry {
69 name: ":status",
70 value: "400",
71 },
72 StaticTableEntry {
73 name: ":status",
74 value: "404",
75 },
76 StaticTableEntry {
77 name: ":status",
78 value: "500",
79 },
80 StaticTableEntry {
81 name: "accept-charset",
82 value: "",
83 },
84 StaticTableEntry {
85 name: "accept-encoding",
86 value: "gzip, deflate",
87 },
88 StaticTableEntry {
89 name: "accept-language",
90 value: "",
91 },
92 StaticTableEntry {
93 name: "accept-ranges",
94 value: "",
95 },
96 StaticTableEntry {
97 name: "accept",
98 value: "",
99 },
100 StaticTableEntry {
101 name: "access-control-allow-origin",
102 value: "",
103 },
104 StaticTableEntry {
105 name: "age",
106 value: "",
107 },
108 StaticTableEntry {
109 name: "allow",
110 value: "",
111 },
112 StaticTableEntry {
113 name: "authorization",
114 value: "",
115 },
116 StaticTableEntry {
117 name: "cache-control",
118 value: "",
119 },
120 StaticTableEntry {
121 name: "content-disposition",
122 value: "",
123 },
124 StaticTableEntry {
125 name: "content-encoding",
126 value: "",
127 },
128 StaticTableEntry {
129 name: "content-language",
130 value: "",
131 },
132 StaticTableEntry {
133 name: "content-length",
134 value: "",
135 },
136 StaticTableEntry {
137 name: "content-location",
138 value: "",
139 },
140 StaticTableEntry {
141 name: "content-range",
142 value: "",
143 },
144 StaticTableEntry {
145 name: "content-type",
146 value: "",
147 },
148 StaticTableEntry {
149 name: "cookie",
150 value: "",
151 },
152 StaticTableEntry {
153 name: "date",
154 value: "",
155 },
156 StaticTableEntry {
157 name: "etag",
158 value: "",
159 },
160 StaticTableEntry {
161 name: "expect",
162 value: "",
163 },
164 StaticTableEntry {
165 name: "expires",
166 value: "",
167 },
168 StaticTableEntry {
169 name: "from",
170 value: "",
171 },
172 StaticTableEntry {
173 name: "host",
174 value: "",
175 },
176 StaticTableEntry {
177 name: "if-match",
178 value: "",
179 },
180 StaticTableEntry {
181 name: "if-modified-since",
182 value: "",
183 },
184 StaticTableEntry {
185 name: "if-none-match",
186 value: "",
187 },
188 StaticTableEntry {
189 name: "if-range",
190 value: "",
191 },
192 StaticTableEntry {
193 name: "if-unmodified-since",
194 value: "",
195 },
196 StaticTableEntry {
197 name: "last-modified",
198 value: "",
199 },
200 StaticTableEntry {
201 name: "link",
202 value: "",
203 },
204 StaticTableEntry {
205 name: "location",
206 value: "",
207 },
208 StaticTableEntry {
209 name: "max-forwards",
210 value: "",
211 },
212 StaticTableEntry {
213 name: "proxy-authenticate",
214 value: "",
215 },
216 StaticTableEntry {
217 name: "proxy-authorization",
218 value: "",
219 },
220 StaticTableEntry {
221 name: "range",
222 value: "",
223 },
224 StaticTableEntry {
225 name: "referer",
226 value: "",
227 },
228 StaticTableEntry {
229 name: "refresh",
230 value: "",
231 },
232 StaticTableEntry {
233 name: "retry-after",
234 value: "",
235 },
236 StaticTableEntry {
237 name: "server",
238 value: "",
239 },
240 StaticTableEntry {
241 name: "set-cookie",
242 value: "",
243 },
244 StaticTableEntry {
245 name: "strict-transport-security",
246 value: "",
247 },
248 StaticTableEntry {
249 name: "transfer-encoding",
250 value: "",
251 },
252 StaticTableEntry {
253 name: "user-agent",
254 value: "",
255 },
256 StaticTableEntry {
257 name: "vary",
258 value: "",
259 },
260 StaticTableEntry {
261 name: "via",
262 value: "",
263 },
264 StaticTableEntry {
265 name: "www-authenticate",
266 value: "",
267 },
268];
269
270#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct HeaderField {
273 pub name: String,
275 pub value: String,
277}
278
279impl HeaderField {
280 #[must_use]
282 pub fn size(&self) -> usize {
283 32 + self.name.len() + self.value.len()
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct Table {
292 dynamic: VecDeque<HeaderField>,
293 size: usize,
295 max_size: usize,
297}
298
299impl Default for Table {
300 fn default() -> Self {
301 Self {
302 dynamic: VecDeque::new(),
303 size: 0,
304 max_size: 4096, }
306 }
307}
308
309impl Table {
310 #[must_use]
312 pub fn new(max_size: usize) -> Self {
313 Self {
314 dynamic: VecDeque::new(),
315 size: 0,
316 max_size,
317 }
318 }
319
320 pub fn add(&mut self, field: HeaderField) {
323 let new_size = field.size();
324 if new_size > self.max_size {
325 self.dynamic.clear();
327 self.size = 0;
328 return;
329 }
330 while self.size + new_size > self.max_size {
332 if let Some(removed) = self.dynamic.pop_back() {
333 self.size -= removed.size();
334 } else {
335 break;
336 }
337 }
338 self.size += new_size;
339 self.dynamic.push_front(field);
340 }
341
342 #[must_use]
344 pub fn get(&self, index: usize) -> Option<HeaderField> {
345 if index == 0 {
346 return None;
347 }
348 if index <= STATIC_TABLE.len() {
349 let e = STATIC_TABLE[index - 1];
350 return Some(HeaderField {
351 name: alloc::string::ToString::to_string(e.name),
352 value: alloc::string::ToString::to_string(e.value),
353 });
354 }
355 let dyn_index = index - STATIC_TABLE.len() - 1;
356 self.dynamic.get(dyn_index).cloned()
357 }
358
359 #[must_use]
363 pub fn find(&self, name: &str, value: &str) -> Option<(usize, bool)> {
364 let mut name_only: Option<usize> = None;
365 for (i, e) in STATIC_TABLE.iter().enumerate() {
367 if e.name == name {
368 if e.value == value {
369 return Some((i + 1, true));
370 }
371 name_only.get_or_insert(i + 1);
372 }
373 }
374 for (i, h) in self.dynamic.iter().enumerate() {
376 let idx = STATIC_TABLE.len() + 1 + i;
377 if h.name == name {
378 if h.value == value {
379 return Some((idx, true));
380 }
381 name_only.get_or_insert(idx);
382 }
383 }
384 name_only.map(|i| (i, false))
385 }
386
387 #[must_use]
389 pub fn size(&self) -> usize {
390 self.size
391 }
392
393 #[must_use]
395 pub fn max_size(&self) -> usize {
396 self.max_size
397 }
398
399 pub fn set_max_size(&mut self, new_max: usize) {
402 self.max_size = new_max;
403 while self.size > self.max_size {
404 if let Some(removed) = self.dynamic.pop_back() {
405 self.size -= removed.size();
406 } else {
407 break;
408 }
409 }
410 }
411
412 #[must_use]
414 pub fn len(&self) -> usize {
415 self.dynamic.len()
416 }
417
418 #[must_use]
420 pub fn is_empty(&self) -> bool {
421 self.dynamic.is_empty()
422 }
423}
424
425#[cfg(test)]
426#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
427mod tests {
428 use super::*;
429 use alloc::string::ToString;
430
431 fn hf(n: &str, v: &str) -> HeaderField {
432 HeaderField {
433 name: n.into(),
434 value: v.into(),
435 }
436 }
437
438 #[test]
439 fn static_table_has_61_entries() {
440 assert_eq!(STATIC_TABLE.len(), 61);
441 assert_eq!(STATIC_TABLE[0].name, ":authority");
442 assert_eq!(STATIC_TABLE[60].name, "www-authenticate");
443 }
444
445 #[test]
446 fn lookup_static_index_1() {
447 let t = Table::new(4096);
448 let h = t.get(1).unwrap();
449 assert_eq!(h.name, ":authority");
450 assert_eq!(h.value, "");
451 }
452
453 #[test]
454 fn lookup_static_index_2_get() {
455 let t = Table::new(4096);
456 let h = t.get(2).unwrap();
457 assert_eq!(h.name, ":method");
458 assert_eq!(h.value, "GET");
459 }
460
461 #[test]
462 fn dynamic_add_and_lookup() {
463 let mut t = Table::new(4096);
464 t.add(hf("custom", "value"));
465 assert_eq!(t.len(), 1);
466 let h = t.get(62).unwrap();
467 assert_eq!(h.name, "custom");
468 assert_eq!(h.value, "value");
469 }
470
471 #[test]
472 fn dynamic_evicts_oldest_on_overflow() {
473 let mut t = Table::new(64); t.add(hf("a", "1")); t.add(hf("b", "2")); assert_eq!(t.len(), 1);
477 assert_eq!(t.get(62).unwrap().name, "b");
478 }
479
480 #[test]
481 fn entry_too_large_clears_table() {
482 let mut t = Table::new(50);
483 t.add(hf("a", "1"));
484 let huge = hf("very-long-name-that-exceeds-max", "value");
485 t.add(huge);
486 assert!(t.is_empty(), "table should be cleared per Spec §4.4");
487 }
488
489 #[test]
490 fn find_static_full_match() {
491 let t = Table::new(4096);
492 let r = t.find(":method", "GET");
493 assert_eq!(r, Some((2, true)));
494 }
495
496 #[test]
497 fn find_static_name_only() {
498 let t = Table::new(4096);
499 let r = t.find(":method", "PATCH");
500 assert!(r.unwrap().0 == 2 || r.unwrap().0 == 3);
501 assert!(!r.unwrap().1);
502 }
503
504 #[test]
505 fn find_dynamic() {
506 let mut t = Table::new(4096);
507 t.add(hf("custom", "value"));
508 let r = t.find("custom", "value");
509 assert_eq!(r, Some((62, true)));
510 }
511
512 #[test]
513 fn set_max_size_shrinks_evicting() {
514 let mut t = Table::new(4096);
515 t.add(hf("a", "1"));
516 t.add(hf("b", "2"));
517 t.set_max_size(34);
518 assert!(t.len() <= 1);
519 }
520
521 #[test]
522 fn header_field_size_is_32_plus_name_plus_value() {
523 let h = hf("abc", "12345");
524 assert_eq!(h.size(), 32 + 3 + 5);
525 }
526
527 #[test]
528 fn lookup_zero_index_returns_none() {
529 let t = Table::new(4096);
530 assert!(t.get(0).is_none());
531 }
532
533 #[test]
534 fn find_for_name_string() {
535 let t = Table::new(4096);
536 let r = t.find(":method", "GET");
537 let h = t.get(r.unwrap().0).unwrap();
538 assert_eq!(h.value.to_string(), "GET");
539 }
540}