ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 PC_ENABLE_HTTP2
16
17#include "network_drivers/presentation/codec/hpack_prim/hpack_prim.h" // shared prefix-int + Huffman
18#include <string.h>
19
20namespace
21{
22#define HPACK_BYTES PC_HPACK_TABLE_BYTES
23#define HPACK_ENTS PC_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 {
97 t->ring[(pos + i) % HPACK_BYTES] = src[i];
98 }
99}
100void ring_read(const HpackDynTable *t, uint16_t pos, char *dst, size_t n)
101{
102 for (size_t i = 0; i < n; i++)
103 {
104 dst[i] = t->ring[(pos + i) % HPACK_BYTES];
105 }
106}
107
108// Descriptor for the k-th newest live entry (k = 1 is newest); null if out of range.
109// k<1 is dead: both callers pass idx-61 with idx>61 already established (idx<=61 takes the static
110// branch in resolve_name/emit_indexed), so k>=1 always holds here.
111const HpackEntry *dyn_entry(const HpackDynTable *t, uint32_t k)
112{
113 if (k < 1 || k > t->ecount) // GCOVR_EXCL_BR_LINE
114 {
115 return nullptr;
116 }
117 uint16_t di = (uint16_t)((t->ehead + HPACK_ENTS - k) % HPACK_ENTS);
118 return &t->ent[di];
119}
120
121// Unreachable: both call sites below only invoke this inside a `while (... && t->ecount > 0)` loop.
122void dyn_evict_oldest(HpackDynTable *t)
123{
124 if (t->ecount == 0) // GCOVR_EXCL_LINE
125 {
126 return; // GCOVR_EXCL_LINE
127 }
128 uint16_t oi = (uint16_t)((t->ehead + HPACK_ENTS - t->ecount) % HPACK_ENTS);
129 const HpackEntry *e = &t->ent[oi];
130 uint16_t bytes = (uint16_t)(e->name_len + e->val_len);
131 t->rtail = (uint16_t)((t->rtail + bytes) % HPACK_BYTES);
132 t->rused = (uint16_t)(t->rused - bytes);
133 t->used -= (uint32_t)e->name_len + e->val_len + 32;
134 t->ecount--;
135}
136
137void dyn_set_max(HpackDynTable *t, uint32_t new_max)
138{
139 if (new_max > HPACK_BYTES)
140 {
141 new_max = HPACK_BYTES; // never exceed our advertised storage
142 }
143 t->max_size = new_max;
144 // ecount>0 is dead here: ecount==0 implies used==0 (dyn_insert/dyn_evict_oldest keep that
145 // invariant), so used>max_size can't be true while ecount==0.
146 while (t->used > t->max_size && t->ecount > 0) // GCOVR_EXCL_BR_LINE
147 {
148 dyn_evict_oldest(t);
149 }
150}
151
152void dyn_insert(HpackDynTable *t, const char *name, size_t nlen, const char *val, size_t vlen)
153{
154 uint32_t entry_size = (uint32_t)nlen + (uint32_t)vlen + 32;
155 if (entry_size > t->max_size)
156 { // RFC 7541 sec 4.4: clears the table, entry not added
157 t->ecount = 0;
158 t->used = 0;
159 t->rused = 0;
160 t->rtail = 0;
161 t->ehead = 0;
162 return;
163 }
164 // Both the entry-count half of the || and the trailing ecount>0 check are dead: entry_size <=
165 // t->max_size is already guaranteed above, and PC_HPACK_TABLE_BYTES / 32 == PC_HPACK_MAX_ENTRIES
166 // exactly, so the byte-budget half always trips at or before the entry-count half could, and
167 // ecount can't be 0 whenever either half of the || is true.
168 while ((t->used + entry_size > t->max_size || t->ecount >= HPACK_ENTS) && t->ecount > 0) // GCOVR_EXCL_BR_LINE
169 {
170 dyn_evict_oldest(t);
171 }
172 uint16_t rpos = (uint16_t)((t->rtail + t->rused) % HPACK_BYTES);
173 HpackEntry *e = &t->ent[t->ehead];
174 e->name_len = (uint16_t)nlen;
175 e->val_len = (uint16_t)vlen;
176 e->ring_pos = rpos;
177 ring_write(t, rpos, name, nlen);
178 ring_write(t, (uint16_t)((rpos + nlen) % HPACK_BYTES), val, vlen);
179 t->rused = (uint16_t)(t->rused + nlen + vlen);
180 t->ehead = (uint16_t)((t->ehead + 1) % HPACK_ENTS);
181 t->ecount++;
182 t->used += entry_size;
183}
184
185// Copy an indexed header's name (idx>=1) into out; false if idx invalid / too big.
186// idx>=1 is always true here: the only caller (decode_literal) routes name_idx==0 to the inline-name
187// path and never calls resolve_name with it.
188bool resolve_name(const HpackDynTable *t, uint32_t idx, char *out, size_t cap, size_t *out_len)
189{
190 if (idx >= 1 && idx <= 61) // GCOVR_EXCL_BR_LINE
191 {
192 size_t nl = strnlen(STATIC[idx][0], cap + 1);
193 if (nl > cap)
194 {
195 return false;
196 }
197 memcpy(out, STATIC[idx][0], nl);
198 *out_len = nl;
199 return true;
200 }
201 const HpackEntry *e = dyn_entry(t, idx - 61);
202 if (!e || e->name_len > cap)
203 {
204 return false;
205 }
206 ring_read(t, e->ring_pos, out, e->name_len);
207 *out_len = e->name_len;
208 return true;
209}
210
211// Emit a fully-indexed field (idx resolves to name+value); copies both into scratch.
212// idx>=1 is always true here: the only caller (pc_hpack_decode's 6.1 Indexed Header Field case)
213// already rejects idx==0 before calling emit_indexed.
214bool emit_indexed(HpackDynTable *t, uint32_t idx, char *scratch, size_t cap, HpackEmitFn emit, void *ctx)
215{
216 size_t nl;
217 size_t vl;
218 if (idx >= 1 && idx <= 61) // GCOVR_EXCL_BR_LINE
219 {
220 nl = strnlen(STATIC[idx][0], cap + 1);
221 vl = strnlen(STATIC[idx][1], cap + 1);
222 if (nl + vl > cap)
223 {
224 return false;
225 }
226 memcpy(scratch, STATIC[idx][0], nl);
227 memcpy(scratch + nl, STATIC[idx][1], vl);
228 }
229 else
230 {
231 const HpackEntry *e = dyn_entry(t, idx - 61);
232 if (!e)
233 {
234 return false;
235 }
236 nl = e->name_len;
237 vl = e->val_len;
238 if (nl + vl > cap)
239 {
240 return false;
241 }
242 ring_read(t, e->ring_pos, scratch, nl);
243 ring_read(t, (uint16_t)((e->ring_pos + nl) % HPACK_BYTES), scratch + nl, vl);
244 }
245 return emit(ctx, scratch, nl, scratch + nl, vl);
246}
247
248// Decode a literal representation (name via index or inline, value inline; optional indexing).
249bool decode_literal(HpackDynTable *t, const uint8_t *block, size_t len, size_t *pos, uint8_t prefix_bits, bool do_index,
250 char *scratch, size_t cap, HpackEmitFn emit, void *ctx)
251{
252 size_t c = 0;
253 uint32_t name_idx = 0;
254 if (!pc_hpack_decode_int(block + *pos, len - *pos, prefix_bits, &c, &name_idx))
255 {
256 return false;
257 }
258 *pos += c;
259 size_t name_len = 0;
260 if (name_idx == 0)
261 {
262 if (!pc_hpack_decode_str(block, len, pos, scratch, cap, &name_len))
263 {
264 return false;
265 }
266 }
267 else if (!resolve_name(t, name_idx, scratch, cap, &name_len))
268 {
269 return false;
270 }
271 size_t val_len = 0;
272 if (!pc_hpack_decode_str(block, len, pos, scratch + name_len, cap - name_len, &val_len))
273 {
274 return false;
275 }
276 if (do_index)
277 {
278 dyn_insert(t, scratch, name_len, scratch + name_len, val_len);
279 }
280 return emit(ctx, scratch, name_len, scratch + name_len, val_len);
281}
282
283} // namespace
284
285// --- Public API ------------------------------------------------------------------------------
286
287void pc_hpack_dyn_init(HpackDynTable *t, uint32_t max_bytes)
288{
289 memset(t, 0, sizeof(*t));
290 t->max_size = max_bytes ? max_bytes : (uint32_t)HPACK_BYTES;
291 if (t->max_size > HPACK_BYTES)
292 {
293 t->max_size = HPACK_BYTES;
294 }
295}
296
297bool pc_hpack_decode(HpackDynTable *t, const uint8_t *block, size_t len, char *scratch, size_t scratch_cap,
298 HpackEmitFn emit, void *ctx)
299{
300 size_t pos = 0;
301 while (pos < len)
302 {
303 uint8_t b = block[pos];
304 if (b & 0x80)
305 { // 6.1 Indexed Header Field
306 size_t c = 0;
307 uint32_t idx = 0;
308 if (!pc_hpack_decode_int(block + pos, len - pos, 7, &c, &idx) || idx == 0)
309 {
310 return false;
311 }
312 pos += c;
313 if (!emit_indexed(t, idx, scratch, scratch_cap, emit, ctx))
314 {
315 return false;
316 }
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 {
322 return false;
323 }
324 }
325 else if ((b & 0xE0) == 0x20)
326 { // 6.3 Dynamic table size update (prefix 5)
327 size_t c = 0;
328 uint32_t nm = 0;
329 if (!pc_hpack_decode_int(block + pos, len - pos, 5, &c, &nm))
330 {
331 return false;
332 }
333 pos += c;
334 dyn_set_max(t, nm);
335 }
336 else
337 { // 6.2.2 without / 6.2.3 never indexed (name prefix 4, no table insert)
338 if (!decode_literal(t, block, len, &pos, 4, false, scratch, scratch_cap, emit, ctx))
339 {
340 return false;
341 }
342 }
343 }
344 return true;
345}
346
347size_t pc_hpack_encode_header(uint8_t *out, size_t cap, const char *name, size_t name_len, const char *value,
348 size_t value_len)
349{
350 int name_idx = 0;
351 int full_idx = 0;
352 for (int i = 1; i <= 61; i++)
353 {
354 if (strnlen(STATIC[i][0], name_len + 1) == name_len && memcmp(STATIC[i][0], name, name_len) == 0)
355 {
356 if (!name_idx)
357 {
358 name_idx = i;
359 }
360 if (strnlen(STATIC[i][1], value_len + 1) == value_len && memcmp(STATIC[i][1], value, value_len) == 0)
361 {
362 full_idx = i;
363 break;
364 }
365 }
366 }
367 if (full_idx)
368 {
369 return pc_hpack_encode_int(out, cap, 7, 0x80, (uint32_t)full_idx);
370 }
371 // Literal without indexing (top nibble 0000), name prefix 4.
372 size_t o = pc_hpack_encode_int(out, cap, 4, 0x00, (uint32_t)name_idx);
373 if (!o)
374 {
375 return 0;
376 }
377 if (name_idx == 0)
378 {
379 size_t ns = pc_hpack_encode_str(out + o, cap - o, name, name_len);
380 if (!ns)
381 {
382 return 0;
383 }
384 o += ns;
385 }
386 size_t vs = pc_hpack_encode_str(out + o, cap - o, value, value_len);
387 if (!vs)
388 {
389 return 0;
390 }
391 return o + vs;
392}
393
394#endif // PC_ENABLE_HTTP2
HPACK header compression for HTTP/2 (RFC 7541).