ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
httpcache.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 httpcache.cpp
6 * @brief Cache-Control builder / parser / freshness implementation (see httpcache.h).
7 */
8
9#include "httpcache.h"
10
11#if PC_ENABLE_HTTP_CACHE
12
13#include <string.h>
14
15static const size_t CC_SENT = (size_t)-1; // overflow sentinel threaded through the emitters
16
17void cache_control_init(pc_cache_control *cc)
18{
19 cc->cc_public = false;
20 cc->cc_private = false;
21 cc->no_store = false;
22 cc->no_cache = false;
23 cc->no_transform = false;
24 cc->must_revalidate = false;
25 cc->proxy_revalidate = false;
26 cc->must_understand = false;
27 cc->cc_immutable = false;
28 cc->only_if_cached = false;
29 cc->max_age = -1;
30 cc->s_maxage = -1;
31 cc->stale_while_revalidate = -1;
32 cc->stale_if_error = -1;
33 cc->max_stale = -1;
34 cc->min_fresh = -1;
35}
36
37// --- build -----------------------------------------------------------------
38
39static size_t cc_emit_uint(char *buf, size_t cap, size_t n, unsigned v)
40{
41 char rev[10];
42 int ri = 0;
43 if (v == 0)
44 {
45 rev[ri++] = '0';
46 }
47 else
48 {
49 while (v)
50 {
51 rev[ri++] = (char)('0' + (v % 10));
52 v /= 10;
53 }
54 }
55 if (n + (size_t)ri > cap)
56 {
57 return CC_SENT;
58 }
59 for (int k = 0; k < ri; k++)
60 {
61 buf[n + k] = rev[ri - 1 - k];
62 }
63 return n + (size_t)ri;
64}
65
66// Emit one bare token (with the ", " separator before all but the first).
67static size_t cc_tok(char *buf, size_t cap, size_t n, bool *first, const char *tok)
68{
69 if (n == CC_SENT)
70 {
71 return CC_SENT;
72 }
73 size_t tlen = strnlen(tok, cap);
74 size_t need = (*first ? 0 : 2) + tlen;
75 if (n + need > cap)
76 {
77 return CC_SENT;
78 }
79 if (!*first)
80 {
81 buf[n++] = ',';
82 buf[n++] = ' ';
83 }
84 memcpy(buf + n, tok, tlen);
85 *first = false;
86 return n + tlen;
87}
88
89// Emit "key=value".
90static size_t cc_kv(char *buf, size_t cap, size_t n, bool *first, const char *key, long v)
91{
92 n = cc_tok(buf, cap, n, first, key);
93 if (n == CC_SENT || n >= cap)
94 {
95 return CC_SENT;
96 }
97 buf[n++] = '=';
98 return cc_emit_uint(buf, cap, n, (unsigned)v);
99}
100
101size_t cache_control_build(char *buf, size_t cap, const pc_cache_control *cc)
102{
103 if (!buf || !cc || cap == 0)
104 {
105 return 0;
106 }
107 size_t n = 0;
108 bool first = true;
109
110 if (cc->cc_public)
111 {
112 n = cc_tok(buf, cap, n, &first, "public");
113 }
114 if (cc->cc_private)
115 {
116 n = cc_tok(buf, cap, n, &first, "private");
117 }
118 if (cc->no_store)
119 {
120 n = cc_tok(buf, cap, n, &first, "no-store");
121 }
122 if (cc->no_cache)
123 {
124 n = cc_tok(buf, cap, n, &first, "no-cache");
125 }
126 if (cc->max_age >= 0)
127 {
128 n = cc_kv(buf, cap, n, &first, "max-age", cc->max_age);
129 }
130 if (cc->s_maxage >= 0)
131 {
132 n = cc_kv(buf, cap, n, &first, "s-maxage", cc->s_maxage);
133 }
134 if (cc->must_revalidate)
135 {
136 n = cc_tok(buf, cap, n, &first, "must-revalidate");
137 }
138 if (cc->proxy_revalidate)
139 {
140 n = cc_tok(buf, cap, n, &first, "proxy-revalidate");
141 }
142 if (cc->no_transform)
143 {
144 n = cc_tok(buf, cap, n, &first, "no-transform");
145 }
146 if (cc->must_understand)
147 {
148 n = cc_tok(buf, cap, n, &first, "must-understand");
149 }
150 if (cc->cc_immutable)
151 {
152 n = cc_tok(buf, cap, n, &first, "immutable");
153 }
154 if (cc->stale_while_revalidate >= 0)
155 {
156 n = cc_kv(buf, cap, n, &first, "stale-while-revalidate", cc->stale_while_revalidate);
157 }
158 if (cc->stale_if_error >= 0)
159 {
160 n = cc_kv(buf, cap, n, &first, "stale-if-error", cc->stale_if_error);
161 }
162 if (cc->only_if_cached)
163 {
164 n = cc_tok(buf, cap, n, &first, "only-if-cached");
165 }
166 if (cc->max_stale == -2)
167 {
168 n = cc_tok(buf, cap, n, &first, "max-stale");
169 }
170 else if (cc->max_stale >= 0)
171 {
172 n = cc_kv(buf, cap, n, &first, "max-stale", cc->max_stale);
173 }
174 if (cc->min_fresh >= 0)
175 {
176 n = cc_kv(buf, cap, n, &first, "min-fresh", cc->min_fresh);
177 }
178
179 if (n == CC_SENT || first || n + 1 > cap)
180 {
181 return 0; // overflow, or nothing was emitted, or no room for the NUL
182 }
183 buf[n] = 0;
184 return n;
185}
186
187// --- parse -----------------------------------------------------------------
188
189// Case-insensitive compare of [s,s+len) to the (lowercase) NUL-terminated @p target.
190static bool cc_ci_eq(const char *s, size_t len, const char *target)
191{
192 size_t i = 0;
193 for (; i < len && target[i]; i++)
194 {
195 char c = s[i];
196 if (c >= 'A' && c <= 'Z')
197 {
198 c = (char)(c + 32);
199 }
200 if (c != target[i])
201 {
202 return false;
203 }
204 }
205 return i == len && target[i] == 0;
206}
207
208// Parse a non-negative delta-seconds from [v,v+vlen) (tolerates surrounding quotes / spaces).
209// Returns the value clamped to INT32_MAX, or -1 if no digits are present.
210static int32_t cc_parse_delta(const char *v, size_t vlen)
211{
212 if (!v)
213 {
214 return -1;
215 }
216 size_t i = 0;
217 while (i < vlen && (v[i] == ' ' || v[i] == '\t' || v[i] == '"'))
218 {
219 i++;
220 }
221 // int64_t, not long: long is 32-bit on LLP64 (Windows), so `val * 10` overflowed (signed UB, wrapped)
222 // before the clamp below could ever observe a value above INT32_MAX. Clamping every iteration keeps
223 // val <= INT32_MAX, so the next multiply-add stays far inside int64_t no matter how many digits arrive.
224 int64_t val = -1;
225 bool any = false;
226 while (i < vlen && v[i] >= '0' && v[i] <= '9')
227 {
228 if (!any)
229 {
230 val = 0;
231 any = true;
232 }
233 val = val * 10 + (v[i] - '0');
234 if (val > 2147483647)
235 {
236 val = 2147483647;
237 }
238 i++;
239 }
240 return any ? (int32_t)val : -1;
241}
242
243static bool cc_match(pc_cache_control *cc, const char *name, size_t nlen, const char *val, size_t vlen)
244{
245 if (cc_ci_eq(name, nlen, "public"))
246 {
247 cc->cc_public = true;
248 }
249 else if (cc_ci_eq(name, nlen, "private"))
250 {
251 cc->cc_private = true;
252 }
253 else if (cc_ci_eq(name, nlen, "no-store"))
254 {
255 cc->no_store = true;
256 }
257 else if (cc_ci_eq(name, nlen, "no-cache"))
258 {
259 cc->no_cache = true;
260 }
261 else if (cc_ci_eq(name, nlen, "no-transform"))
262 {
263 cc->no_transform = true;
264 }
265 else if (cc_ci_eq(name, nlen, "must-revalidate"))
266 {
267 cc->must_revalidate = true;
268 }
269 else if (cc_ci_eq(name, nlen, "proxy-revalidate"))
270 {
271 cc->proxy_revalidate = true;
272 }
273 else if (cc_ci_eq(name, nlen, "must-understand"))
274 {
275 cc->must_understand = true;
276 }
277 else if (cc_ci_eq(name, nlen, "immutable"))
278 {
279 cc->cc_immutable = true;
280 }
281 else if (cc_ci_eq(name, nlen, "only-if-cached"))
282 {
283 cc->only_if_cached = true;
284 }
285 else if (cc_ci_eq(name, nlen, "max-age"))
286 {
287 cc->max_age = cc_parse_delta(val, vlen);
288 }
289 else if (cc_ci_eq(name, nlen, "s-maxage"))
290 {
291 cc->s_maxage = cc_parse_delta(val, vlen);
292 }
293 else if (cc_ci_eq(name, nlen, "stale-while-revalidate"))
294 {
295 cc->stale_while_revalidate = cc_parse_delta(val, vlen);
296 }
297 else if (cc_ci_eq(name, nlen, "stale-if-error"))
298 {
299 cc->stale_if_error = cc_parse_delta(val, vlen);
300 }
301 else if (cc_ci_eq(name, nlen, "max-stale"))
302 {
303 cc->max_stale = val ? cc_parse_delta(val, vlen) : -2; // present with no value = "any"
304 }
305 else if (cc_ci_eq(name, nlen, "min-fresh"))
306 {
307 cc->min_fresh = cc_parse_delta(val, vlen);
308 }
309 else
310 {
311 return false; // unknown directive - ignored
312 }
313 return true;
314}
315
316// Parse one comma-separated directive starting at *i (advancing past it) and apply it; returns true
317// if a known directive matched.
318static bool cache_parse_one_directive(const char *s, size_t len, size_t *i, pc_cache_control *cc)
319{
320 while (*i < len && (s[*i] == ',' || s[*i] == ' ' || s[*i] == '\t'))
321 {
322 (*i)++; // skip separators / OWS
323 }
324 if (*i >= len)
325 {
326 return false;
327 }
328 size_t start = *i;
329 while (*i < len && s[*i] != ',')
330 {
331 (*i)++; // to the next comma
332 }
333 size_t end = *i;
334 // `end > start` cannot go false: the skip-separators loop above already advanced *i past every
335 // leading ',', ' ', and '\t', so s[start] is never one of those, which forces the "to the next
336 // comma" loop to advance *i at least one past start - end is always >= start+1 on entry, and
337 // since s[start] itself is never a space/tab, this trim can never step end back down to start.
338 while (end > start && // GCOVR_EXCL_BR_LINE end==start unreachable (see above)
339 (s[end - 1] == ' ' || s[end - 1] == '\t'))
340 {
341 end--; // trim trailing OWS
342 }
343 size_t eq = start;
344 while (eq < end && s[eq] != '=')
345 {
346 eq++;
347 }
348 size_t nlen = (eq < end ? eq : end) - start;
349 while (nlen > 0 && (s[start + nlen - 1] == ' ' || s[start + nlen - 1] == '\t'))
350 {
351 nlen--; // trim trailing OWS from name
352 }
353 const char *val = (eq < end) ? s + eq + 1 : nullptr;
354 size_t vlen = (eq < end) ? end - (eq + 1) : 0;
355 return nlen && cc_match(cc, s + start, nlen, val, vlen);
356}
357
358bool cache_control_parse(const char *s, size_t len, pc_cache_control *cc)
359{
360 cache_control_init(cc);
361 if (!s)
362 {
363 return false;
364 }
365 bool found = false;
366 size_t i = 0;
367 while (i < len)
368 {
369 if (cache_parse_one_directive(s, len, &i, cc))
370 {
371 found = true;
372 }
373 }
374 return found;
375}
376
377// --- presets + freshness ---------------------------------------------------
378
379void cache_immutable_asset(pc_cache_control *cc, uint32_t max_age)
380{
381 cache_control_init(cc);
382 cc->cc_public = true;
383 cc->max_age = (int32_t)(max_age > 2147483647u ? 2147483647u : max_age);
384 cc->cc_immutable = true;
385}
386
387void cache_revalidatable(pc_cache_control *cc, uint32_t max_age, int32_t stale_while_revalidate)
388{
389 cache_control_init(cc);
390 cc->cc_public = true;
391 cc->max_age = (int32_t)(max_age > 2147483647u ? 2147483647u : max_age);
392 if (stale_while_revalidate >= 0)
393 {
394 cc->stale_while_revalidate = stale_while_revalidate;
395 }
396}
397
398void cache_no_store(pc_cache_control *cc)
399{
400 cache_control_init(cc);
401 cc->no_store = true;
402}
403
404void cache_shared(pc_cache_control *cc, uint32_t max_age, uint32_t s_maxage)
405{
406 cache_control_init(cc);
407 cc->cc_public = true;
408 cc->max_age = (int32_t)(max_age > 2147483647u ? 2147483647u : max_age);
409 cc->s_maxage = (int32_t)(s_maxage > 2147483647u ? 2147483647u : s_maxage);
410}
411
412long cache_freshness_lifetime(const pc_cache_control *cc, bool shared, long expires_minus_date)
413{
414 if (shared && cc->s_maxage >= 0)
415 {
416 return cc->s_maxage;
417 }
418 if (cc->max_age >= 0)
419 {
420 return cc->max_age;
421 }
422 if (expires_minus_date >= 0)
423 {
424 return expires_minus_date;
425 }
426 return -1; // no explicit expiration - the caller applies a heuristic
427}
428
429#endif // PC_ENABLE_HTTP_CACHE
HTTP Cache-Control directive builder + parser + freshness helper (RFC 9111), PC_ENABLE_HTTP_CACHE.