ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
edge_cache.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 edge_cache.cpp
6 * @brief CDN edge-cache tier - pure engine. See edge_cache.h.
7 */
8
10
11#if PC_ENABLE_EDGE_CACHE
12
13#include "crypto/hash/sha256.h"
14#include <string.h>
15
16namespace
17{
18char lc(char c)
19{
20 return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
21}
22
23// Read a run of decimal digits at *pp into @p out; advance *pp. False if no digit is present.
24bool rd_uint(const char **pp, int *out)
25{
26 const char *p = *pp;
27 if (*p < '0' || *p > '9')
28 {
29 return false;
30 }
31 int v = 0;
32 while (*p >= '0' && *p <= '9')
33 {
34 v = v * 10 + (*p - '0');
35 p++;
36 }
37 *pp = p;
38 *out = v;
39 return true;
40}
41
42// Read a 3-letter month abbreviation at *pp -> 1..12; advance past it. False if unrecognized.
43bool rd_month(const char **pp, int *out)
44{
45 static const char MONTHS[] = "janfebmaraprmayjunjulaugsepoctnovdec";
46 const char *p = *pp;
47 char a = lc(p[0]);
48 char b = lc(p[1]);
49 char c = lc(p[2]);
50 for (int m = 0; m < 12; m++)
51 {
52 if (a == MONTHS[m * 3] && b == MONTHS[m * 3 + 1] && c == MONTHS[m * 3 + 2])
53 {
54 *pp = p + 3;
55 *out = m + 1;
56 return true;
57 }
58 }
59 return false;
60}
61
62// Read "hh:mm:ss" at *pp; advance past it.
63bool rd_time(const char **pp, int *hh, int *mm, int *ss)
64{
65 const char *p = *pp;
66 if (!rd_uint(&p, hh) || *p != ':')
67 {
68 return false;
69 }
70 p++;
71 if (!rd_uint(&p, mm) || *p != ':')
72 {
73 return false;
74 }
75 p++;
76 if (!rd_uint(&p, ss))
77 {
78 return false;
79 }
80 *pp = p;
81 return true;
82}
83
84// Days since 1970-01-01 for a proleptic-Gregorian y-m-d (Howard Hinnant's civil algorithm).
85int64_t days_from_civil(int y, int m, int d)
86{
87 if (m <= 2)
88 {
89 y -= 1;
90 }
91 int64_t era = (y >= 0 ? y : y - 399) / 400;
92 int64_t yoe = y - era * 400; // [0, 399]
93 int64_t doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; // [0, 365]
94 int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
95 return era * 146097 + doe - 719468;
96}
97
98// Append @p s to out[*pos..cap), optionally lowercased. False (and no write) on overflow.
99bool k_append(char *out, size_t *pos, size_t cap, const char *s, bool lower)
100{
101 size_t p = *pos;
102 for (const char *q = s; *q; q++)
103 {
104 if (p + 1 >= cap) // leave room for the terminating NUL
105 {
106 return false;
107 }
108 out[p++] = lower ? lc(*q) : *q;
109 }
110 *pos = p;
111 return true;
112}
113} // namespace
114
115namespace
116{
117// If the header line [p, lend) is "<name>: <value>" (case-insensitive name), copy the OWS-trimmed value
118// into out[0..out_cap) and return true. Non-match returns false with no write; a value that will not fit
119// sets *overflow (the caller must then fail the whole lookup - a validator is never truncated).
120bool header_line_value(const char *p, const char *lend, const char *name, size_t namelen, char *out, size_t out_cap,
121 bool *overflow)
122{
123 const char *colon = p;
124 while (colon < lend && *colon != ':')
125 {
126 colon++;
127 }
128 if (colon >= lend || (size_t)(colon - p) != namelen)
129 {
130 return false;
131 }
132 for (size_t i = 0; i < namelen; i++)
133 {
134 if (lc(p[i]) != lc(name[i]))
135 {
136 return false;
137 }
138 }
139 const char *v = colon + 1;
140 while (v < lend && (*v == ' ' || *v == '\t'))
141 {
142 v++;
143 }
144 const char *ve = lend;
145 while (ve > v && (ve[-1] == ' ' || ve[-1] == '\t'))
146 {
147 ve--;
148 }
149 size_t vl = (size_t)(ve - v);
150 if (vl >= out_cap)
151 {
152 *overflow = true;
153 return false; // never truncate a validator
154 }
155 for (size_t i = 0; i < vl; i++)
156 {
157 out[i] = v[i];
158 }
159 out[vl] = '\0';
160 return true;
161}
162} // namespace
163
164bool edge_header_value(const char *hdrs, size_t len, const char *name, char *out, size_t out_cap)
165{
166 if (!hdrs || !name || !out || out_cap == 0)
167 {
168 return false;
169 }
170 out[0] = '\0';
171 size_t namelen = strnlen(name, out_cap); // header names are short literals, always < the value buffer
172 const char *p = hdrs;
173 const char *end = hdrs + len;
174 // Skip the status line.
175 while (p < end && *p != '\n')
176 {
177 p++;
178 }
179 if (p < end)
180 {
181 p++;
182 }
183 while (p < end)
184 {
185 if (*p == '\r' || *p == '\n') // blank line: end of the header block
186 {
187 break;
188 }
189 const char *le = p;
190 while (le < end && *le != '\n')
191 {
192 le++;
193 }
194 const char *lend = le; // exclusive; drop a trailing CR
195 // `lend > p` has no false arm to reach: a line starting with CR or LF is the blank line that
196 // ends the header block and already broke out above, so p is always on a content byte and
197 // the line is at least one byte long. The guard stays to keep lend[-1] in bounds.
198 if (lend > p && lend[-1] == '\r') // GCOVR_EXCL_LINE - lend > p always true here, see above
199 {
200 lend--;
201 }
202 bool overflow = false;
203 if (header_line_value(p, lend, name, namelen, out, out_cap, &overflow))
204 {
205 return true;
206 }
207 if (overflow)
208 {
209 return false;
210 }
211 p = (le < end) ? le + 1 : end;
212 }
213 return false;
214}
215
216namespace
217{
218// IMF-fixdate "Sun, 06 Nov 1994 08:49:37 GMT" or RFC 850 "Sunday, 06-Nov-94 08:49:37 GMT" (from past ',').
219bool parse_date_after_comma(const char *p, int *mday, int *mon, int *year, int *hh, int *mm, int *ss)
220{
221 while (*p == ' ')
222 {
223 p++;
224 }
225 if (!rd_uint(&p, mday))
226 {
227 return false;
228 }
229 bool rfc850 = (*p == '-');
230 if (*p == '-' || *p == ' ')
231 {
232 p++;
233 }
234 else
235 {
236 return false;
237 }
238 if (!rd_month(&p, mon))
239 {
240 return false;
241 }
242 if (*p == '-')
243 {
244 p++;
245 }
246 else
247 {
248 while (*p == ' ')
249 {
250 p++;
251 }
252 }
253 if (!rd_uint(&p, year))
254 {
255 return false;
256 }
257 if (rfc850 && *year < 100) // 2-digit year window (RFC 6265-style)
258 {
259 *year += (*year < 70) ? 2000 : 1900;
260 }
261 while (*p == ' ')
262 {
263 p++;
264 }
265 return rd_time(&p, hh, mm, ss);
266}
267
268// asctime "Sun Nov 6 08:49:37 1994".
269bool parse_date_asctime(const char *p, int *mday, int *mon, int *year, int *hh, int *mm, int *ss)
270{
271 while (*p && *p != ' ')
272 {
273 p++;
274 }
275 while (*p == ' ')
276 {
277 p++;
278 }
279 if (!rd_month(&p, mon))
280 {
281 return false;
282 }
283 while (*p == ' ')
284 {
285 p++;
286 }
287 if (!rd_uint(&p, mday))
288 {
289 return false;
290 }
291 while (*p == ' ')
292 {
293 p++;
294 }
295 if (!rd_time(&p, hh, mm, ss))
296 {
297 return false;
298 }
299 while (*p == ' ')
300 {
301 p++;
302 }
303 return rd_uint(&p, year);
304}
305} // namespace
306
307int64_t edge_parse_http_date(const char *s, size_t len)
308{
309 if (!s)
310 {
311 return -1;
312 }
313 char buf[64];
314 while (len && (*s == ' ' || *s == '\t')) // trim leading OWS
315 {
316 s++;
317 len--;
318 }
319 if (len == 0 || len >= sizeof(buf))
320 {
321 return -1;
322 }
323 memcpy(buf, s, len);
324 buf[len] = '\0';
325
326 int mday = 0;
327 int mon = 0;
328 int year = 0;
329 int hh = 0;
330 int mm = 0;
331 int ss = 0;
332 const char *comma = strchr(buf, ',');
333 bool ok = comma ? parse_date_after_comma(comma + 1, &mday, &mon, &year, &hh, &mm, &ss)
334 : parse_date_asctime(buf, &mday, &mon, &year, &hh, &mm, &ss);
335 if (!ok)
336 {
337 return -1;
338 }
339
340 // GCOVR_EXCL_START unreachable: rd_month() only ever yields 1..12 (and both date parsers fail
341 // outright when it does not match), so this guard is defensive. Kept because it is the one place
342 // that would otherwise let an out-of-range month reach days_from_civil(). The five range checks
343 // below ARE reachable and are pinned by tests, so they stay outside this exclusion.
344 if (mon < 1 || mon > 12)
345 {
346 return -1;
347 }
348 // GCOVR_EXCL_STOP
349 if (mday < 1 || mday > 31 || hh > 23 || mm > 59 || ss > 60)
350 {
351 return -1;
352 }
353 int64_t days = days_from_civil(year, mon, mday);
354 return days * 86400 + (int64_t)hh * 3600 + (int64_t)mm * 60 + ss;
355}
356
357long edge_freshness_lifetime(const pc_cache_control *cc, bool shared, int64_t date_epoch, int64_t expires_epoch)
358{
359 long expires_minus_date = -1;
360 if (date_epoch >= 0 && expires_epoch >= 0)
361 {
362 expires_minus_date = (long)(expires_epoch - date_epoch);
363 }
364 return cache_freshness_lifetime(cc, shared, expires_minus_date);
365}
366
367long edge_heuristic_lifetime(int64_t date_epoch, int64_t last_modified_epoch)
368{
369 if (date_epoch < 0 || last_modified_epoch < 0 || last_modified_epoch >= date_epoch)
370 {
371 return -1;
372 }
373 return (long)((date_epoch - last_modified_epoch) / 10); // RFC 9111 sec 4.2.2 (10%)
374}
375
376long edge_initial_age(int32_t age_hdr, int64_t date_epoch, int64_t response_time_epoch)
377{
378 long apparent = 0;
379 if (date_epoch >= 0 && response_time_epoch >= 0 && response_time_epoch > date_epoch)
380 {
381 apparent = (long)(response_time_epoch - date_epoch);
382 }
383 long corrected = (age_hdr > 0) ? (long)age_hdr : 0;
384 return (apparent > corrected) ? apparent : corrected;
385}
386
387long edge_current_age(long initial_age, uint32_t insert_ms, uint32_t now_ms)
388{
389 uint32_t resident_ms = now_ms - insert_ms; // unsigned: wrap-safe
390 return initial_age + (long)(resident_ms / 1000u);
391}
392
393bool edge_is_fresh_at(long lifetime, long current_age)
394{
395 return lifetime >= 0 && current_age < lifetime;
396}
397
398size_t edge_key_canon(const char *method, const char *host, const char *path, const char *query, bool include_query,
399 char *out, size_t out_cap)
400{
401 if (!method || !host || !path || !out || out_cap == 0)
402 {
403 return 0;
404 }
405 size_t pos = 0;
406 if (!k_append(out, &pos, out_cap, method, false))
407 {
408 return 0;
409 }
410 if (!k_append(out, &pos, out_cap, "\n", false) || !k_append(out, &pos, out_cap, host, true))
411 {
412 return 0;
413 }
414 if (!k_append(out, &pos, out_cap, "\n", false) || !k_append(out, &pos, out_cap, path, false))
415 {
416 return 0;
417 }
418 if (include_query && query && query[0] &&
419 (!k_append(out, &pos, out_cap, "\n", false) || !k_append(out, &pos, out_cap, query, false)))
420 {
421 return 0;
422 }
423 out[pos] = '\0';
424 return pos;
425}
426
427void edge_key_digest(const char *canon, size_t len, uint8_t digest[32])
428{
429 pc_sha256((const uint8_t *)canon, len, digest);
430}
431
432namespace
433{
434// Parse one Vary field-name token at *pp (advancing past it) and, when non-empty, emit its
435// "name\x1e value \x1f" record to out so distinct names cannot alias and a present-but-empty value is
436// distinguished from an absent one. Returns false on "Vary: *" (uncacheable) or on overflow.
437bool vary_emit_one(const char **pp, EdgeHdrLookup lookup, void *ctx, char *out, size_t *pos, size_t out_cap)
438{
439 const char *p = *pp;
440 char name[48];
441 size_t nl = 0;
442 while (*p && *p != ',' && *p != ' ' && *p != '\t')
443 {
444 if (*p == '*') // Vary: * -> uncacheable
445 {
446 return false;
447 }
448 if (nl + 1 < sizeof(name))
449 {
450 name[nl++] = lc(*p);
451 }
452 p++;
453 }
454 name[nl] = '\0';
455 *pp = p;
456 // GCOVR_EXCL_START unreachable: edge_vary_serialize() skips every separator and breaks on NUL
457 // before calling in, so *p is always a content byte here and the loop above has already taken at
458 // least one character (or returned false on '*'). nl is therefore never 0.
459 if (nl == 0)
460 {
461 return true; // nothing to emit; caller advances
462 }
463 // GCOVR_EXCL_STOP
464 const char *val = lookup ? lookup(ctx, name) : nullptr;
465 if (!k_append(out, pos, out_cap, name, false) || !k_append(out, pos, out_cap, "\x1e", false))
466 {
467 return false;
468 }
469 if (val && !k_append(out, pos, out_cap, val, false))
470 {
471 return false;
472 }
473 return k_append(out, pos, out_cap, "\x1f", false);
474}
475} // namespace
476
477bool edge_vary_serialize(const char *vary_header, EdgeHdrLookup lookup, void *ctx, char *out, size_t out_cap)
478{
479 if (!out || out_cap == 0)
480 {
481 return false;
482 }
483 out[0] = '\0';
484 if (!vary_header)
485 {
486 return true; // no Vary -> empty key
487 }
488 size_t pos = 0;
489 const char *p = vary_header;
490 while (*p)
491 {
492 while (*p == ' ' || *p == '\t' || *p == ',')
493 {
494 p++;
495 }
496 if (!*p)
497 {
498 break;
499 }
500 if (!vary_emit_one(&p, lookup, ctx, out, &pos, out_cap))
501 {
502 return false;
503 }
504 }
505 out[pos] = '\0';
506 return true;
507}
508
509// --- L1 store ------------------------------------------------------------------------------------
510
511namespace
512{
513void lru_unlink(EdgeCacheStore *s, uint16_t i)
514{
515 EdgeEntry *e = &s->entries[i];
516 if (e->lru.prev != PC_EDGE_LRU_NONE)
517 {
518 s->entries[e->lru.prev].lru.next = e->lru.next;
519 }
520 else
521 {
522 s->lru_head = e->lru.next;
523 }
524 if (e->lru.next != PC_EDGE_LRU_NONE)
525 {
526 s->entries[e->lru.next].lru.prev = e->lru.prev;
527 }
528 else
529 {
530 s->lru_tail = e->lru.prev;
531 }
532 e->lru.next = PC_EDGE_LRU_NONE;
533 e->lru.prev = PC_EDGE_LRU_NONE;
534}
535
536void lru_push_front(EdgeCacheStore *s, uint16_t i)
537{
538 EdgeEntry *e = &s->entries[i];
539 e->lru.prev = PC_EDGE_LRU_NONE;
540 e->lru.next = s->lru_head;
541 if (s->lru_head != PC_EDGE_LRU_NONE)
542 {
543 s->entries[s->lru_head].lru.prev = i;
544 }
545 s->lru_head = i;
546 if (s->lru_tail == PC_EDGE_LRU_NONE)
547 {
548 s->lru_tail = i;
549 }
550}
551
552void store_free(EdgeCacheStore *s, uint16_t i)
553{
554 lru_unlink(s, i);
555 s->entries[i].used = false;
556}
557
558// The path portion of a canonical key "METHOD\nhost\npath[\nquery]" (after the 2nd '\n'), or nullptr.
559const char *key_path(const char *key)
560{
561 int nl = 0;
562 for (const char *p = key; *p; p++)
563 {
564 if (*p != '\n')
565 {
566 continue;
567 }
568 if (++nl == 2)
569 {
570 return p + 1;
571 }
572 }
573 return nullptr;
574}
575
576bool vary_is_star(const char *vary_header)
577{
578 if (!vary_header)
579 {
580 return false;
581 }
582 for (const char *p = vary_header; *p; p++)
583 {
584 if (*p == '*')
585 {
586 return true;
587 }
588 }
589 return false;
590}
591} // namespace
592
593void edge_store_init(EdgeCacheStore *s)
594{
595 memset(s, 0, sizeof(*s));
596 s->lru_head = PC_EDGE_LRU_NONE;
597 s->lru_tail = PC_EDGE_LRU_NONE;
598 for (uint16_t i = 0; i < PC_EDGE_CACHE_SLOTS; i++)
599 {
600 s->entries[i].lru.prev = PC_EDGE_LRU_NONE;
601 s->entries[i].lru.next = PC_EDGE_LRU_NONE;
602 }
603}
604
605EdgeEntry *edge_store_alloc(EdgeCacheStore *s, const char *canon, const char *vary_key)
606{
607 size_t klen = strnlen(canon, sizeof(s->entries[0].key));
608 if (klen >= sizeof(s->entries[0].key))
609 {
610 return nullptr; // key too long -> non-cacheable
611 }
612 uint16_t slot = PC_EDGE_LRU_NONE;
613 for (uint16_t i = 0; i < PC_EDGE_CACHE_SLOTS; i++)
614 {
615 if (!s->entries[i].used)
616 {
617 slot = i;
618 break;
619 }
620 }
621 if (slot == PC_EDGE_LRU_NONE)
622 {
623 if (s->lru_tail == PC_EDGE_LRU_NONE)
624 {
625 return nullptr; // PC_EDGE_CACHE_SLOTS == 0
626 }
627 slot = s->lru_tail;
628 // Offer the still-populated victim to the L2 write-back hook (skip transient passthrough slots).
629 if (s->on_evict && s->entries[slot].key[0] != '\0')
630 {
631 s->on_evict(s->evict_ctx, &s->entries[slot]);
632 }
633 store_free(s, slot);
634 s->stats.evictions++;
635 }
636 EdgeEntry *e = &s->entries[slot];
637 memset(e, 0, sizeof(*e));
638 e->lru.prev = PC_EDGE_LRU_NONE;
639 e->lru.next = PC_EDGE_LRU_NONE;
640 e->used = true;
641 memcpy(e->key, canon, klen);
642 e->key[klen] = '\0';
643 edge_key_digest(canon, klen, e->digest);
644 size_t vl = vary_key ? strnlen(vary_key, sizeof(e->vary_vals)) : 0;
645 if (vl >= sizeof(e->vary_vals))
646 {
647 vl = sizeof(e->vary_vals) - 1;
648 }
649 if (vary_key)
650 {
651 memcpy(e->vary_vals, vary_key, vl);
652 }
653 e->vary_vals[vl] = '\0';
654 e->date_epoch = -1;
655 e->expires_epoch = -1;
656 lru_push_front(s, slot);
657 s->stats.stores++;
658 return e;
659}
660
661EdgeEntry *edge_store_lookup(EdgeCacheStore *s, const char *canon, const char *vary_key, uint32_t now_ms)
662{
663 const char *vk = vary_key ? vary_key : "";
664 for (uint16_t i = 0; i < PC_EDGE_CACHE_SLOTS; i++)
665 {
666 EdgeEntry *e = &s->entries[i];
667 if (e->used && strcmp(e->key, canon) == 0 && strcmp(e->vary_vals, vk) == 0)
668 {
669 lru_unlink(s, i);
670 lru_push_front(s, i);
671 e->last_used_ms = now_ms;
672 return e;
673 }
674 }
675 return nullptr;
676}
677
678EdgeEntry *edge_store_find(EdgeCacheStore *s, const char *canon, EdgeHdrLookup lookup, void *ctx, uint32_t now_ms)
679{
680 for (uint16_t i = 0; i < PC_EDGE_CACHE_SLOTS; i++)
681 {
682 EdgeEntry *e = &s->entries[i];
683 if (!e->used || strcmp(e->key, canon) != 0)
684 {
685 continue;
686 }
687 char cur[PC_EDGE_VARY_MAX];
688 // re-serialize the current request against this variant's Vary names (empty names -> "")
689 if (!edge_vary_serialize(e->vary_names, lookup, ctx, cur, sizeof(cur)))
690 {
691 continue;
692 }
693 if (strcmp(cur, e->vary_vals) == 0)
694 {
695 lru_unlink(s, i);
696 lru_push_front(s, i);
697 e->last_used_ms = now_ms;
698 return e;
699 }
700 }
701 return nullptr;
702}
703
704void edge_entry_set_freshness(EdgeEntry *e, const pc_cache_control *cc, bool shared, int64_t date_epoch,
705 int64_t expires_epoch, int64_t last_modified_epoch, int32_t age_hdr,
706 int64_t response_time_epoch, uint32_t now_ms)
707{
708 long lifetime = edge_freshness_lifetime(cc, shared, date_epoch, expires_epoch);
709 if (lifetime < 0)
710 {
711 lifetime = edge_heuristic_lifetime(date_epoch, last_modified_epoch);
712 }
713 if (lifetime < 0)
714 {
715 lifetime = PC_EDGE_DEFAULT_TTL_S;
716 }
717 e->lifetime_s = lifetime;
718 e->initial_age = edge_initial_age(age_hdr, date_epoch, response_time_epoch);
719 e->insert_ms = now_ms;
720 e->date_epoch = date_epoch;
721 e->expires_epoch = expires_epoch;
722 e->age_hdr = (age_hdr > 0) ? age_hdr : 0;
723}
724
725bool edge_entry_has_validator(const EdgeEntry *e)
726{
727 return e->etag[0] != '\0' || e->last_modified[0] != '\0';
728}
729
730bool edge_entry_fresh(const EdgeEntry *e, uint32_t now_ms)
731{
732 return edge_is_fresh_at(e->lifetime_s, edge_current_age(e->initial_age, e->insert_ms, now_ms));
733}
734
735uint32_t edge_store_sweep(EdgeCacheStore *s, uint32_t now_ms)
736{
737 uint32_t n = 0;
738 for (uint16_t i = 0; i < PC_EDGE_CACHE_SLOTS; i++)
739 {
740 const EdgeEntry *e = &s->entries[i];
741 if (e->used && !edge_entry_has_validator(e) && !edge_entry_fresh(e, now_ms))
742 {
743 store_free(s, i);
744 s->stats.evictions++;
745 n++;
746 }
747 }
748 return n;
749}
750
751uint32_t edge_store_purge(EdgeCacheStore *s, const char *canon)
752{
753 uint32_t n = 0;
754 for (uint16_t i = 0; i < PC_EDGE_CACHE_SLOTS; i++)
755 {
756 if (s->entries[i].used && strcmp(s->entries[i].key, canon) == 0)
757 {
758 store_free(s, i);
759 s->stats.purges++;
760 n++;
761 }
762 }
763 return n;
764}
765
766uint32_t edge_store_purge_prefix(EdgeCacheStore *s, const char *prefix)
767{
768 size_t plen = strnlen(prefix, sizeof(s->entries[0].key));
769 uint32_t n = 0;
770 for (uint16_t i = 0; i < PC_EDGE_CACHE_SLOTS; i++)
771 {
772 if (!s->entries[i].used)
773 {
774 continue;
775 }
776 const char *path = key_path(s->entries[i].key);
777 if (path && strncmp(path, prefix, plen) == 0)
778 {
779 store_free(s, i);
780 s->stats.purges++;
781 n++;
782 }
783 }
784 return n;
785}
786
787void edge_store_free_entry(EdgeCacheStore *s, const EdgeEntry *e)
788{
789 for (uint16_t i = 0; i < PC_EDGE_CACHE_SLOTS; i++)
790 {
791 if (&s->entries[i] == e)
792 {
793 store_free(s, i);
794 return;
795 }
796 }
797}
798
799bool edge_is_storeable(int status, const char *method, const pc_cache_control *cc, const char *vary_header,
800 size_t body_len)
801{
802 if (!method || strcmp(method, "GET") != 0)
803 {
804 return false;
805 }
806 if (status != 200)
807 {
808 return false; // v1: only 200 (other cacheable-by-default statuses are a follow-up)
809 }
810 if (cc && (cc->no_store || cc->cc_private))
811 {
812 return false;
813 }
814 if (vary_is_star(vary_header))
815 {
816 return false;
817 }
818 if (body_len > PC_EDGE_BODY_MAX)
819 {
820 return false;
821 }
822 return true;
823}
824
825// --- conditional revalidation --------------------------------------------------------------------
826
827namespace
828{
829// Append "<name>: <value>\r\n" to out[*pos..cap). False (and no write) on overflow.
830bool hdr_line(char *out, size_t *pos, size_t cap, const char *name, const char *value)
831{
832 return k_append(out, pos, cap, name, false) && k_append(out, pos, cap, ": ", false) &&
833 k_append(out, pos, cap, value, false) && k_append(out, pos, cap, "\r\n", false);
834}
835} // namespace
836
837size_t edge_build_conditional(const EdgeEntry *e, char *out, size_t cap)
838{
839 if (!out || cap == 0)
840 {
841 return 0;
842 }
843 size_t pos = 0;
844 if (e->etag[0] && !hdr_line(out, &pos, cap, "If-None-Match", e->etag))
845 {
846 return 0;
847 }
848 if (e->last_modified[0] && !hdr_line(out, &pos, cap, "If-Modified-Since", e->last_modified))
849 {
850 return 0;
851 }
852 out[pos] = '\0';
853 return pos;
854}
855
856void edge_apply_304(EdgeEntry *e, const char *new_hdrs, size_t hdr_len, int64_t response_time_epoch, uint32_t now_ms)
857{
858 char v[128];
859 pc_cache_control cc;
860 if (edge_header_value(new_hdrs, hdr_len, "Cache-Control", v, sizeof(v)))
861 {
862 cache_control_parse(v, strnlen(v, sizeof(v)), &cc);
863 }
864 else
865 {
866 cache_control_init(&cc);
867 }
868
869 int64_t date = -1;
870 if (edge_header_value(new_hdrs, hdr_len, "Date", v, sizeof(v)))
871 {
872 date = edge_parse_http_date(v, strnlen(v, sizeof(v)));
873 }
874 int64_t expires = -1;
875 if (edge_header_value(new_hdrs, hdr_len, "Expires", v, sizeof(v)))
876 {
877 expires = edge_parse_http_date(v, strnlen(v, sizeof(v)));
878 }
879
880 int32_t age = 0;
881 if (edge_header_value(new_hdrs, hdr_len, "Age", v, sizeof(v)))
882 {
883 long a = 0;
884 bool any = false;
885 for (const char *p = v; *p >= '0' && *p <= '9'; p++)
886 {
887 a = a * 10 + (*p - '0');
888 any = true;
889 }
890 if (any)
891 {
892 age = (int32_t)a;
893 }
894 }
895
896 // Adopt any validators the 304 carried (RFC 9111 4.3.4: the newer representation metadata wins).
897 if (edge_header_value(new_hdrs, hdr_len, "ETag", v, sizeof(v)))
898 {
899 size_t vlen = strnlen(v, sizeof(v));
900 if (vlen < sizeof(e->etag))
901 {
902 memcpy(e->etag, v, vlen + 1);
903 }
904 }
905 int64_t last_mod = -1;
906 if (edge_header_value(new_hdrs, hdr_len, "Last-Modified", v, sizeof(v)))
907 {
908 size_t vlen = strnlen(v, sizeof(v));
909 last_mod = edge_parse_http_date(v, vlen);
910 if (vlen < sizeof(e->last_modified))
911 {
912 memcpy(e->last_modified, v, vlen + 1);
913 }
914 }
915 else if (e->last_modified[0])
916 {
917 last_mod = edge_parse_http_date(e->last_modified, strnlen(e->last_modified, sizeof(e->last_modified)));
918 }
919
920 edge_entry_set_freshness(e, &cc, true, date, expires, last_mod, age, response_time_epoch, now_ms);
921}
922
923#endif // PC_ENABLE_EDGE_CACHE
#define PC_EDGE_BODY_MAX
#define PC_EDGE_CACHE_SLOTS
CDN edge-cache tier - pure engine (PC_ENABLE_EDGE_CACHE).
#define PC_EDGE_VARY_MAX
#define PC_EDGE_DEFAULT_TTL_S
void pc_sha256(const uint8_t *data, size_t len, uint8_t digest[PC_SHA256_DIGEST_LEN])
One-shot SHA-256: hash len bytes of data into digest (32 bytes).
Definition sha256.cpp:58
SHA-256 (FIPS 180-4) - streaming context and one-shot API.