ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
qpack.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 qpack.cpp
6 * @brief QPACK (RFC 9204) - implementation. See qpack.h.
7 *
8 * The static table is generated verbatim from RFC 9204 Appendix A (0-indexed). The prefix-integer
9 * and Huffman primitives are shared with HPACK via pc_hpack_prim.h. No dynamic table is maintained:
10 * we encode only against the static table and reject any dynamic-table reference on decode.
11 */
12
14
15#if PC_ENABLE_HTTP3
16
17#include "network_drivers/presentation/codec/hpack_prim/hpack_prim.h" // shared prefix-int + Huffman
18#include <string.h>
19
20namespace
21{
22
23// QPACK static table (RFC 9204 Appendix A, 0-indexed). {name, value}. Generated from the RFC.
24const char *const QPACK_STATIC[99][2] = {
25 {":authority", ""},
26 {":path", "/"},
27 {"age", "0"},
28 {"content-disposition", ""},
29 {"content-length", "0"},
30 {"cookie", ""},
31 {"date", ""},
32 {"etag", ""},
33 {"if-modified-since", ""},
34 {"if-none-match", ""},
35 {"last-modified", ""},
36 {"link", ""},
37 {"location", ""},
38 {"referer", ""},
39 {"set-cookie", ""},
40 {":method", "CONNECT"},
41 {":method", "DELETE"},
42 {":method", "GET"},
43 {":method", "HEAD"},
44 {":method", "OPTIONS"},
45 {":method", "POST"},
46 {":method", "PUT"},
47 {":scheme", "http"},
48 {":scheme", "https"},
49 {":status", "103"},
50 {":status", "200"},
51 {":status", "304"},
52 {":status", "404"},
53 {":status", "503"},
54 {"accept", "*/*"},
55 {"accept", "application/dns-message"},
56 {"accept-encoding", "gzip, deflate, br"},
57 {"accept-ranges", "bytes"},
58 {"access-control-allow-headers", "cache-control"},
59 {"access-control-allow-headers", "content-type"},
60 {"access-control-allow-origin", "*"},
61 {"cache-control", "max-age=0"},
62 {"cache-control", "max-age=2592000"},
63 {"cache-control", "max-age=604800"},
64 {"cache-control", "no-cache"},
65 {"cache-control", "no-store"},
66 {"cache-control", "public, max-age=31536000"},
67 {"content-encoding", "br"},
68 {"content-encoding", "gzip"},
69 {"content-type", "application/dns-message"},
70 {"content-type", "application/javascript"},
71 {"content-type", "application/json"},
72 {"content-type", "application/x-www-form-urlencoded"},
73 {"content-type", "image/gif"},
74 {"content-type", "image/jpeg"},
75 {"content-type", "image/png"},
76 {"content-type", "text/css"},
77 {"content-type", "text/html;charset=utf-8"},
78 {"content-type", "text/plain"},
79 {"content-type", "text/plain;charset=utf-8"},
80 {"range", "bytes=0-"},
81 {"strict-transport-security", "max-age=31536000"},
82 {"strict-transport-security", "max-age=31536000;includesubdomains"},
83 {"strict-transport-security", "max-age=31536000;includesubdomains;preload"},
84 {"vary", "accept-encoding"},
85 {"vary", "origin"},
86 {"x-content-type-options", "nosniff"},
87 {"x-xss-protection", "1; mode=block"},
88 {":status", "100"},
89 {":status", "204"},
90 {":status", "206"},
91 {":status", "302"},
92 {":status", "400"},
93 {":status", "403"},
94 {":status", "421"},
95 {":status", "425"},
96 {":status", "500"},
97 {"accept-language", ""},
98 {"access-control-allow-credentials", "FALSE"},
99 {"access-control-allow-credentials", "TRUE"},
100 {"access-control-allow-headers", "*"},
101 {"access-control-allow-methods", "get"},
102 {"access-control-allow-methods", "get, post, options"},
103 {"access-control-allow-methods", "options"},
104 {"access-control-expose-headers", "content-length"},
105 {"access-control-request-headers", "content-type"},
106 {"access-control-request-method", "get"},
107 {"access-control-request-method", "post"},
108 {"alt-svc", "clear"},
109 {"authorization", ""},
110 {"content-security-policy", "script-src 'none';object-src 'none';base-uri 'none'"},
111 {"early-data", "1"},
112 {"expect-ct", ""},
113 {"forwarded", ""},
114 {"if-range", ""},
115 {"origin", ""},
116 {"purpose", "prefetch"},
117 {"server", ""},
118 {"timing-allow-origin", "*"},
119 {"upgrade-insecure-requests", "1"},
120 {"user-agent", ""},
121 {"x-forwarded-for", ""},
122 {"x-frame-options", "deny"},
123 {"x-frame-options", "sameorigin"},
124};
125
126} // namespace
127
128size_t pc_qpack_encode_prefix(uint8_t *out, size_t cap)
129{
130 if (cap < 2)
131 {
132 return 0;
133 }
134 out[0] = 0x00; // Required Insert Count = 0
135 out[1] = 0x00; // S = 0, Delta Base = 0
136 return 2;
137}
138
139size_t pc_qpack_encode_header(uint8_t *out, size_t cap, const char *name, size_t name_len, const char *value,
140 size_t value_len)
141{
142 int name_idx = -1, full_idx = -1;
143 for (int i = 0; i < 99; i++)
144 {
145 if (strnlen(QPACK_STATIC[i][0], name_len + 1) == name_len && memcmp(QPACK_STATIC[i][0], name, name_len) == 0)
146 {
147 if (name_idx < 0)
148 {
149 name_idx = i;
150 }
151 if (strnlen(QPACK_STATIC[i][1], value_len + 1) == value_len &&
152 memcmp(QPACK_STATIC[i][1], value, value_len) == 0)
153 {
154 full_idx = i;
155 break;
156 }
157 }
158 }
159 if (full_idx >= 0) // Indexed Field Line, static: 1 T=1 i(6)
160 {
161 return pc_hpack_encode_int(out, cap, 6, 0xC0, (uint32_t)full_idx);
162 }
163
164 if (name_idx >= 0)
165 { // Literal Field Line with Name Reference, static: 01 N=0 T=1 i(4)
166 size_t o = pc_hpack_encode_int(out, cap, 4, 0x50, (uint32_t)name_idx);
167 if (!o)
168 {
169 return 0;
170 }
171 size_t vs = pc_hpack_encode_str(out + o, cap - o, value, value_len);
172 if (!vs)
173 {
174 return 0;
175 }
176 return o + vs;
177 }
178
179 // Literal Field Line with Literal Name: 001 N=0 H NameLen(3), name string, value string.
180 size_t hl = pc_hpack_huff_len(name, name_len);
181 bool huff = hl < name_len;
182 size_t nbytes = huff ? hl : name_len;
183 size_t o = pc_hpack_encode_int(out, cap, 3, (uint8_t)(0x20 | (huff ? 0x08 : 0x00)), (uint32_t)nbytes);
184 if (!o)
185 {
186 return 0;
187 }
188 if (huff)
189 {
190 size_t body = pc_hpack_huff_encode(out + o, cap - o, name, name_len);
191 if (body != hl)
192 {
193 return 0;
194 }
195 o += body;
196 }
197 else
198 {
199 if (o + name_len > cap)
200 {
201 return 0;
202 }
203 memcpy(out + o, name, name_len);
204 o += name_len;
205 }
206 size_t vs = pc_hpack_encode_str(out + o, cap - o, value, value_len);
207 if (!vs)
208 {
209 return 0;
210 }
211 return o + vs;
212}
213
214bool pc_qpack_decode(const uint8_t *block, size_t len, char *scratch, size_t scratch_cap, QpackEmitFn emit, void *ctx)
215{
216 size_t pos = 0;
217 // Encoded Field Section Prefix (RFC 9204 sec 4.5.1): Required Insert Count, then S + Delta Base.
218 size_t c = 0;
219 uint32_t ric = 0;
220 if (!pc_hpack_decode_int(block + pos, len - pos, 8, &c, &ric))
221 {
222 return false;
223 }
224 pos += c;
225 if (ric != 0) // a non-zero Required Insert Count references the dynamic table (capacity 0)
226 {
227 return false;
228 }
229 uint32_t base = 0;
230 if (!pc_hpack_decode_int(block + pos, len - pos, 7, &c, &base)) // S bit + Delta Base; ignored when RIC = 0
231 {
232 return false;
233 }
234 pos += c;
235
236 while (pos < len)
237 {
238 uint8_t b = block[pos];
239 if (b & 0x80)
240 { // Indexed Field Line (sec 4.5.2): 1 T i(6)
241 if (!(b & 0x40)) // T = 0 -> dynamic table
242 {
243 return false;
244 }
245 uint32_t idx = 0;
246 if (!pc_hpack_decode_int(block + pos, len - pos, 6, &c, &idx) || idx >= 99)
247 {
248 return false;
249 }
250 pos += c;
251 const char *nm = QPACK_STATIC[idx][0];
252 const char *vl = QPACK_STATIC[idx][1];
253 if (!emit(ctx, nm, strnlen(nm, scratch_cap + 1), vl, strnlen(vl, scratch_cap + 1)))
254 {
255 return false;
256 }
257 }
258 else if ((b & 0xC0) == 0x40)
259 { // Literal Field Line with Name Reference (sec 4.5.4): 01 N T i(4)
260 bool is_static = (b & 0x10) != 0;
261 uint32_t idx = 0;
262 if (!pc_hpack_decode_int(block + pos, len - pos, 4, &c, &idx))
263 {
264 return false;
265 }
266 pos += c;
267 if (!is_static || idx >= 99) // dynamic name reference
268 {
269 return false;
270 }
271 const char *nm = QPACK_STATIC[idx][0];
272 size_t nlen = strnlen(nm, scratch_cap + 1);
273 if (nlen > scratch_cap)
274 {
275 return false;
276 }
277 memcpy(scratch, nm, nlen);
278 size_t vlen = 0;
279 if (!pc_hpack_decode_str(block, len, &pos, scratch + nlen, scratch_cap - nlen, &vlen))
280 {
281 return false;
282 }
283 if (!emit(ctx, scratch, nlen, scratch + nlen, vlen))
284 {
285 return false;
286 }
287 }
288 else if ((b & 0xE0) == 0x20)
289 { // Literal Field Line with Literal Name (sec 4.5.6): 001 N H NameLen(3)
290 bool huff = (b & 0x08) != 0;
291 uint32_t nlen32 = 0;
292 if (!pc_hpack_decode_int(block + pos, len - pos, 3, &c, &nlen32))
293 {
294 return false;
295 }
296 pos += c;
297 if (pos + nlen32 > len)
298 {
299 return false;
300 }
301 size_t nlen = 0;
302 if (huff)
303 {
304 if (!pc_hpack_huff_decode(block + pos, nlen32, scratch, scratch_cap, &nlen))
305 {
306 return false;
307 }
308 }
309 else
310 {
311 if (nlen32 > scratch_cap)
312 {
313 return false;
314 }
315 memcpy(scratch, block + pos, nlen32);
316 nlen = nlen32;
317 }
318 pos += nlen32;
319 size_t vlen = 0;
320 if (!pc_hpack_decode_str(block, len, &pos, scratch + nlen, scratch_cap - nlen, &vlen))
321 {
322 return false;
323 }
324 if (!emit(ctx, scratch, nlen, scratch + nlen, vlen))
325 {
326 return false;
327 }
328 }
329 else
330 { // 0001 xxxx Indexed Post-Base / 0000 xxxx Literal Post-Base Name Ref: both dynamic
331 return false;
332 }
333 }
334 return true;
335}
336
337#endif // PC_ENABLE_HTTP3
QPACK field-section compression for HTTP/3 (RFC 9204).