DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
hpack.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file hpack.cpp
6 * @brief HPACK (RFC 7541) - implementation. See hpack.h.
7 *
8 * The static table, the Huffman code (Appendix B), and the canonical Huffman decode tables are
9 * generated verbatim from RFC 7541 (see docs / the generator in the repo history). Header names
10 * and values never touch the heap; the dynamic table is a fixed byte ring with FIFO eviction.
11 */
12
14
15#if DETWS_ENABLE_HTTP2
16
17#include "network_drivers/presentation/hpack_prim/hpack_prim.h" // shared prefix-int + Huffman
18#include <string.h>
19
20namespace
21{
22#define HPACK_BYTES DETWS_HPACK_TABLE_BYTES
23#define HPACK_ENTS DETWS_HPACK_MAX_ENTRIES
24
25// Static table (1-indexed; entry 0 is a placeholder). {name, value}. Generated from RFC 7541 App A.
26const char *const STATIC[62][2] = {
27 {"", ""},
28 {":authority", ""},
29 {":method", "GET"},
30 {":method", "POST"},
31 {":path", "/"},
32 {":path", "/index.html"},
33 {":scheme", "http"},
34 {":scheme", "https"},
35 {":status", "200"},
36 {":status", "204"},
37 {":status", "206"},
38 {":status", "304"},
39 {":status", "400"},
40 {":status", "404"},
41 {":status", "500"},
42 {"accept-charset", ""},
43 {"accept-encoding", "gzip, deflate"},
44 {"accept-language", ""},
45 {"accept-ranges", ""},
46 {"accept", ""},
47 {"access-control-allow-origin", ""},
48 {"age", ""},
49 {"allow", ""},
50 {"authorization", ""},
51 {"cache-control", ""},
52 {"content-disposition", ""},
53 {"content-encoding", ""},
54 {"content-language", ""},
55 {"content-length", ""},
56 {"content-location", ""},
57 {"content-range", ""},
58 {"content-type", ""},
59 {"cookie", ""},
60 {"date", ""},
61 {"etag", ""},
62 {"expect", ""},
63 {"expires", ""},
64 {"from", ""},
65 {"host", ""},
66 {"if-match", ""},
67 {"if-modified-since", ""},
68 {"if-none-match", ""},
69 {"if-range", ""},
70 {"if-unmodified-since", ""},
71 {"last-modified", ""},
72 {"link", ""},
73 {"location", ""},
74 {"max-forwards", ""},
75 {"proxy-authenticate", ""},
76 {"proxy-authorization", ""},
77 {"range", ""},
78 {"referer", ""},
79 {"refresh", ""},
80 {"retry-after", ""},
81 {"server", ""},
82 {"set-cookie", ""},
83 {"strict-transport-security", ""},
84 {"transfer-encoding", ""},
85 {"user-agent", ""},
86 {"vary", ""},
87 {"via", ""},
88 {"www-authenticate", ""},
89};
90
91// --- Dynamic table byte-ring helpers ---------------------------------------------------------
92
93void ring_write(HpackDynTable *t, uint16_t pos, const char *src, size_t n)
94{
95 for (size_t i = 0; i < n; i++)
96 t->ring[(pos + i) % HPACK_BYTES] = src[i];
97}
98void ring_read(const HpackDynTable *t, uint16_t pos, char *dst, size_t n)
99{
100 for (size_t i = 0; i < n; i++)
101 dst[i] = t->ring[(pos + i) % HPACK_BYTES];
102}
103
104// Descriptor for the k-th newest live entry (k = 1 is newest); null if out of range.
105const HpackEntry *dyn_entry(const HpackDynTable *t, uint32_t k)
106{
107 if (k < 1 || k > t->ecount)
108 return nullptr;
109 uint16_t di = (uint16_t)((t->ehead + HPACK_ENTS - k) % HPACK_ENTS);
110 return &t->ent[di];
111}
112
113void dyn_evict_oldest(HpackDynTable *t)
114{
115 if (t->ecount == 0)
116 return;
117 uint16_t oi = (uint16_t)((t->ehead + HPACK_ENTS - t->ecount) % HPACK_ENTS);
118 const HpackEntry *e = &t->ent[oi];
119 uint16_t bytes = (uint16_t)(e->name_len + e->val_len);
120 t->rtail = (uint16_t)((t->rtail + bytes) % HPACK_BYTES);
121 t->rused = (uint16_t)(t->rused - bytes);
122 t->used -= (uint32_t)e->name_len + e->val_len + 32;
123 t->ecount--;
124}
125
126void dyn_set_max(HpackDynTable *t, uint32_t new_max)
127{
128 if (new_max > HPACK_BYTES)
129 new_max = HPACK_BYTES; // never exceed our advertised storage
130 t->max_size = new_max;
131 while (t->used > t->max_size && t->ecount > 0)
132 dyn_evict_oldest(t);
133}
134
135void dyn_insert(HpackDynTable *t, const char *name, size_t nlen, const char *val, size_t vlen)
136{
137 uint32_t entry_size = (uint32_t)nlen + (uint32_t)vlen + 32;
138 if (entry_size > t->max_size)
139 { // RFC 7541 sec 4.4: clears the table, entry not added
140 t->ecount = 0;
141 t->used = 0;
142 t->rused = 0;
143 t->rtail = 0;
144 t->ehead = 0;
145 return;
146 }
147 while ((t->used + entry_size > t->max_size || t->ecount >= HPACK_ENTS) && t->ecount > 0)
148 dyn_evict_oldest(t);
149 uint16_t rpos = (uint16_t)((t->rtail + t->rused) % HPACK_BYTES);
150 HpackEntry *e = &t->ent[t->ehead];
151 e->name_len = (uint16_t)nlen;
152 e->val_len = (uint16_t)vlen;
153 e->ring_pos = rpos;
154 ring_write(t, rpos, name, nlen);
155 ring_write(t, (uint16_t)((rpos + nlen) % HPACK_BYTES), val, vlen);
156 t->rused = (uint16_t)(t->rused + nlen + vlen);
157 t->ehead = (uint16_t)((t->ehead + 1) % HPACK_ENTS);
158 t->ecount++;
159 t->used += entry_size;
160}
161
162// --- String coding ---------------------------------------------------------------------------
163
164// Read a length-prefixed string at block[*pos] into out; advances *pos.
165bool decode_string(const uint8_t *block, size_t len, size_t *pos, char *out, size_t cap, size_t *out_len)
166{
167 if (*pos >= len)
168 return false;
169 bool huff = (block[*pos] & 0x80) != 0;
170 size_t c = 0;
171 uint32_t slen = 0;
172 if (!hpack_decode_int(block + *pos, len - *pos, 7, &c, &slen))
173 return false;
174 *pos += c;
175 if (*pos + slen > len)
176 return false;
177 if (huff)
178 {
179 if (!hpack_huff_decode(block + *pos, slen, out, cap, out_len))
180 return false;
181 }
182 else
183 {
184 if (slen > cap)
185 return false;
186 memcpy(out, block + *pos, slen);
187 *out_len = slen;
188 }
189 *pos += slen;
190 return true;
191}
192
193size_t encode_string(uint8_t *out, size_t cap, const char *s, size_t n)
194{
195 size_t hl = hpack_huff_len(s, n);
196 if (hl < n)
197 {
198 size_t hdr = hpack_encode_int(out, cap, 7, 0x80, (uint32_t)hl);
199 if (!hdr)
200 return 0;
201 size_t body = hpack_huff_encode(out + hdr, cap - hdr, s, n);
202 if (body != hl)
203 return 0;
204 return hdr + body;
205 }
206 size_t hdr = hpack_encode_int(out, cap, 7, 0x00, (uint32_t)n);
207 if (!hdr || hdr + n > cap)
208 return 0;
209 memcpy(out + hdr, s, n);
210 return hdr + n;
211}
212
213// Copy an indexed header's name (idx>=1) into out; false if idx invalid / too big.
214bool resolve_name(const HpackDynTable *t, uint32_t idx, char *out, size_t cap, size_t *out_len)
215{
216 if (idx >= 1 && idx <= 61)
217 {
218 size_t nl = strnlen(STATIC[idx][0], cap + 1);
219 if (nl > cap)
220 return false;
221 memcpy(out, STATIC[idx][0], nl);
222 *out_len = nl;
223 return true;
224 }
225 const HpackEntry *e = dyn_entry(t, idx - 61);
226 if (!e || e->name_len > cap)
227 return false;
228 ring_read(t, e->ring_pos, out, e->name_len);
229 *out_len = e->name_len;
230 return true;
231}
232
233// Emit a fully-indexed field (idx resolves to name+value); copies both into scratch.
234bool emit_indexed(HpackDynTable *t, uint32_t idx, char *scratch, size_t cap, HpackEmitFn emit, void *ctx)
235{
236 size_t nl;
237 size_t vl;
238 if (idx >= 1 && idx <= 61)
239 {
240 nl = strnlen(STATIC[idx][0], cap + 1);
241 vl = strnlen(STATIC[idx][1], cap + 1);
242 if (nl + vl > cap)
243 return false;
244 memcpy(scratch, STATIC[idx][0], nl);
245 memcpy(scratch + nl, STATIC[idx][1], vl);
246 }
247 else
248 {
249 const HpackEntry *e = dyn_entry(t, idx - 61);
250 if (!e)
251 return false;
252 nl = e->name_len;
253 vl = e->val_len;
254 if (nl + vl > cap)
255 return false;
256 ring_read(t, e->ring_pos, scratch, nl);
257 ring_read(t, (uint16_t)((e->ring_pos + nl) % HPACK_BYTES), scratch + nl, vl);
258 }
259 return emit(ctx, scratch, nl, scratch + nl, vl);
260}
261
262// Decode a literal representation (name via index or inline, value inline; optional indexing).
263bool decode_literal(HpackDynTable *t, const uint8_t *block, size_t len, size_t *pos, uint8_t prefix_bits, bool do_index,
264 char *scratch, size_t cap, HpackEmitFn emit, void *ctx)
265{
266 size_t c = 0;
267 uint32_t name_idx = 0;
268 if (!hpack_decode_int(block + *pos, len - *pos, prefix_bits, &c, &name_idx))
269 return false;
270 *pos += c;
271 size_t name_len = 0;
272 if (name_idx == 0)
273 {
274 if (!decode_string(block, len, pos, scratch, cap, &name_len))
275 return false;
276 }
277 else if (!resolve_name(t, name_idx, scratch, cap, &name_len))
278 {
279 return false;
280 }
281 size_t val_len = 0;
282 if (!decode_string(block, len, pos, scratch + name_len, cap - name_len, &val_len))
283 return false;
284 if (do_index)
285 dyn_insert(t, scratch, name_len, scratch + name_len, val_len);
286 return emit(ctx, scratch, name_len, scratch + name_len, val_len);
287}
288
289} // namespace
290
291// --- Public API ------------------------------------------------------------------------------
292
293void hpack_dyn_init(HpackDynTable *t, uint32_t max_bytes)
294{
295 memset(t, 0, sizeof(*t));
296 t->max_size = max_bytes ? max_bytes : (uint32_t)HPACK_BYTES;
297 if (t->max_size > HPACK_BYTES)
298 t->max_size = HPACK_BYTES;
299}
300
301bool hpack_decode(HpackDynTable *t, const uint8_t *block, size_t len, char *scratch, size_t scratch_cap,
302 HpackEmitFn emit, void *ctx)
303{
304 size_t pos = 0;
305 while (pos < len)
306 {
307 uint8_t b = block[pos];
308 if (b & 0x80)
309 { // 6.1 Indexed Header Field
310 size_t c = 0;
311 uint32_t idx = 0;
312 if (!hpack_decode_int(block + pos, len - pos, 7, &c, &idx) || idx == 0)
313 return false;
314 pos += c;
315 if (!emit_indexed(t, idx, scratch, scratch_cap, emit, ctx))
316 return false;
317 }
318 else if (b & 0x40)
319 { // 6.2.1 Literal with incremental indexing (name prefix 6)
320 if (!decode_literal(t, block, len, &pos, 6, true, scratch, scratch_cap, emit, ctx))
321 return false;
322 }
323 else if ((b & 0xE0) == 0x20)
324 { // 6.3 Dynamic table size update (prefix 5)
325 size_t c = 0;
326 uint32_t nm = 0;
327 if (!hpack_decode_int(block + pos, len - pos, 5, &c, &nm))
328 return false;
329 pos += c;
330 dyn_set_max(t, nm);
331 }
332 else
333 { // 6.2.2 without / 6.2.3 never indexed (name prefix 4, no table insert)
334 if (!decode_literal(t, block, len, &pos, 4, false, scratch, scratch_cap, emit, ctx))
335 return false;
336 }
337 }
338 return true;
339}
340
341size_t hpack_encode_header(uint8_t *out, size_t cap, const char *name, size_t name_len, const char *value,
342 size_t value_len)
343{
344 int name_idx = 0;
345 int full_idx = 0;
346 for (int i = 1; i <= 61; i++)
347 {
348 if (strnlen(STATIC[i][0], name_len + 1) == name_len && memcmp(STATIC[i][0], name, name_len) == 0)
349 {
350 if (!name_idx)
351 name_idx = i;
352 if (strnlen(STATIC[i][1], value_len + 1) == value_len && memcmp(STATIC[i][1], value, value_len) == 0)
353 {
354 full_idx = i;
355 break;
356 }
357 }
358 }
359 if (full_idx)
360 return hpack_encode_int(out, cap, 7, 0x80, (uint32_t)full_idx);
361 // Literal without indexing (top nibble 0000), name prefix 4.
362 size_t o = hpack_encode_int(out, cap, 4, 0x00, (uint32_t)name_idx);
363 if (!o)
364 return 0;
365 if (name_idx == 0)
366 {
367 size_t ns = encode_string(out + o, cap - o, name, name_len);
368 if (!ns)
369 return 0;
370 o += ns;
371 }
372 size_t vs = encode_string(out + o, cap - o, value, value_len);
373 if (!vs)
374 return 0;
375 return o + vs;
376}
377
378#endif // DETWS_ENABLE_HTTP2
HPACK header compression for HTTP/2 (RFC 7541).
Low-level field-coding primitives shared by HPACK and QPACK.