ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
edge_cache_sd.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_sd.cpp
6 * @brief CDN edge-cache tier - L2 SD persistence. See edge_cache_sd.h.
7 */
8
10
11#if PC_ENABLE_EDGE_CACHE
12
13#include <string.h>
14
15namespace
16{
17#define PC_EDGE_SD_VERSION 1
18
19void put_u16(uint8_t *p, uint16_t v)
20{
21 p[0] = (uint8_t)(v & 0xFF);
22 p[1] = (uint8_t)(v >> 8);
23}
24uint16_t get_u16(const uint8_t *p)
25{
26 return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
27}
28
29// Append a u16 length prefix + the NUL-terminated string @p s. False (no write) on overflow.
30bool put_str(uint8_t *out, size_t cap, size_t *pos, const char *s)
31{
32 size_t sl = strnlen(s, cap);
33 // Split from the capacity check below so excluding it does not also drop that check - which IS
34 // reachable and tested - out of the branch measurement. sl is strnlen-capped to `cap`, the
35 // caller's record buffer, which is orders of magnitude below the 16-bit prefix limit in every
36 // build this library is sized for. The check guards the u16 cast if cap ever grows.
37 if (sl > 0xFFFFu) // GCOVR_EXCL_LINE - sl <= cap, far below 0xFFFF, see above
38 {
39 return false; // GCOVR_EXCL_LINE - unreachable body of the guard above
40 }
41 if (*pos + 2 + sl > cap)
42 {
43 return false;
44 }
45 put_u16(out + *pos, (uint16_t)sl);
46 *pos += 2;
47 memcpy(out + *pos, s, sl);
48 *pos += sl;
49 return true;
50}
51
52// Read a u16-prefixed string into @p out (NUL-terminated). False if short or it would not fit (no truncation).
53bool get_str(const uint8_t *buf, size_t len, size_t *pos, char *out, size_t out_cap)
54{
55 if (*pos + 2 > len)
56 {
57 return false;
58 }
59 uint16_t sl = get_u16(buf + *pos);
60 *pos += 2;
61 if (*pos + sl > len || sl >= out_cap)
62 {
63 return false;
64 }
65 memcpy(out, buf + *pos, sl);
66 out[sl] = '\0';
67 *pos += sl;
68 return true;
69}
70} // namespace
71
72size_t edge_sd_serialize(const EdgeEntry *e, uint8_t *out, size_t cap)
73{
74 if (!e || !out || cap < 3)
75 {
76 return 0;
77 }
78 size_t pos = 0;
79 out[pos++] = PC_EDGE_SD_VERSION;
80 put_u16(out + pos, (uint16_t)e->status);
81 pos += 2;
82 if (!put_str(out, cap, &pos, e->key) || !put_str(out, cap, &pos, e->content_type) ||
83 !put_str(out, cap, &pos, e->etag) || !put_str(out, cap, &pos, e->last_modified) ||
84 !put_str(out, cap, &pos, e->content_encoding) || !put_str(out, cap, &pos, e->vary_names) ||
85 !put_str(out, cap, &pos, e->vary_vals))
86 {
87 return 0;
88 }
89 if (pos + 2 + e->body_len > cap)
90 {
91 return 0;
92 }
93 put_u16(out + pos, e->body_len);
94 pos += 2;
95 memcpy(out + pos, e->body, e->body_len);
96 pos += e->body_len;
97 return pos;
98}
99
100bool edge_sd_deserialize(const uint8_t *buf, size_t len, EdgeEntry *e)
101{
102 if (!buf || !e || len < 3 || buf[0] != PC_EDGE_SD_VERSION)
103 {
104 return false;
105 }
106 size_t pos = 1;
107 e->status = get_u16(buf + pos);
108 pos += 2;
109 if (!get_str(buf, len, &pos, e->key, sizeof(e->key)) ||
110 !get_str(buf, len, &pos, e->content_type, sizeof(e->content_type)) ||
111 !get_str(buf, len, &pos, e->etag, sizeof(e->etag)) ||
112 !get_str(buf, len, &pos, e->last_modified, sizeof(e->last_modified)) ||
113 !get_str(buf, len, &pos, e->content_encoding, sizeof(e->content_encoding)) ||
114 !get_str(buf, len, &pos, e->vary_names, sizeof(e->vary_names)) ||
115 !get_str(buf, len, &pos, e->vary_vals, sizeof(e->vary_vals)))
116 {
117 return false;
118 }
119 if (pos + 2 > len)
120 {
121 return false;
122 }
123 uint16_t bl = get_u16(buf + pos);
124 pos += 2;
125 if (bl > PC_EDGE_BODY_MAX || pos + bl > len)
126 {
127 return false;
128 }
129 memcpy(e->body, buf + pos, bl);
130 e->body_len = bl;
131 edge_key_digest(e->key, strnlen(e->key, sizeof(e->key)), e->digest); // re-derive the digest from the key
132 return true;
133}
134
135#if PC_ENABLE_DBM
136
137// The L2 key is the entry's SHA-256 digest, which must fit a dbm key exactly.
138static_assert(PC_DBM_KEY_MAX >= 32, "edge cache L2 uses a 32-byte SHA-256 digest as the dbm key");
139
140namespace
141{
142// The path portion of a canonical key "METHOD\nhost\npath[\nquery]" (after the 2nd '\n'), or nullptr.
143const char *canon_path(const char *canon)
144{
145 int nl = 0;
146 for (const char *p = canon; *p; p++)
147 {
148 if (*p != '\n')
149 {
150 continue;
151 }
152 if (++nl == 2)
153 {
154 return p + 1;
155 }
156 }
157 return nullptr;
158}
159
160// True if @p buf is a valid edge serialization; if so copies its canonical key into @p canon_out.
161bool peek_canon(const uint8_t *buf, size_t len, char *canon_out, size_t cap)
162{
163 if (len < 3 || buf[0] != PC_EDGE_SD_VERSION)
164 {
165 return false;
166 }
167 size_t pos = 3; // version(1) + status(2)
168 return get_str(buf, len, &pos, canon_out, cap);
169}
170
171// Batch of L2 keys collected during an iteration for deletion afterward (dbm forbids delete-in-iterate).
172#define PC_EDGE_SD_PURGE_BATCH 8
173struct CollectCtx
174{
175 pc_dbm *db;
176 const char *prefix; // nullptr = match every edge entry
177 size_t plen;
178 uint8_t *scratch;
179 size_t scratch_cap;
180 uint8_t batch[PC_EDGE_SD_PURGE_BATCH][32];
181 int count;
182 bool full; // hit the batch cap: another pass is needed
183};
184
185bool collect_cb(const char *key, uint16_t key_len, void *vctx)
186{
187 CollectCtx *c = (CollectCtx *)vctx;
188 if (key_len != 32)
189 {
190 return true; // not an edge digest key (shared dbm) - leave it be
191 }
192 long n = pc_dbm_get(c->db, key, key_len, c->scratch, c->scratch_cap);
193 if (n <= 0)
194 {
195 return true;
196 }
197 char canon[PC_EDGE_KEY_MAX];
198 if (!peek_canon(c->scratch, (size_t)n, canon, sizeof(canon)))
199 {
200 return true; // not an edge value - do not touch it
201 }
202 if (c->prefix)
203 {
204 const char *path = canon_path(canon);
205 if (!path || strncmp(path, c->prefix, c->plen) != 0)
206 {
207 return true; // path does not match the purge prefix
208 }
209 }
210 if (c->count >= PC_EDGE_SD_PURGE_BATCH)
211 {
212 c->full = true;
213 return false; // stop this pass; the caller deletes the batch then re-iterates
214 }
215 memcpy(c->batch[c->count++], key, 32);
216 return true;
217}
218
219uint32_t purge_matching(pc_dbm *db, const char *prefix, uint8_t *scratch, size_t scratch_cap)
220{
221 uint32_t total = 0;
222 for (;;)
223 {
224 CollectCtx c;
225 c.db = db;
226 c.prefix = prefix;
227 c.plen = prefix ? strnlen(prefix, PC_EDGE_KEY_MAX) : 0;
228 c.scratch = scratch;
229 c.scratch_cap = scratch_cap;
230 c.count = 0;
231 c.full = false;
232 pc_dbm_iterate(db, collect_cb, &c);
233 for (int i = 0; i < c.count; i++)
234 {
235 if (pc_dbm_del(db, (const char *)c.batch[i], 32))
236 {
237 total++;
238 }
239 }
240 if (!c.full)
241 {
242 break; // visited everything that matched
243 }
244 }
245 return total;
246}
247} // namespace
248
249bool edge_sd_put(pc_dbm *db, const EdgeEntry *e, uint8_t *scratch, size_t scratch_cap)
250{
251 if (!db || !e || !scratch)
252 {
253 return false;
254 }
255 if (!edge_entry_has_validator(e))
256 {
257 return false; // only spill what a cheap 304 can refresh after a reboot
258 }
259 size_t n = edge_sd_serialize(e, scratch, scratch_cap);
260 if (n == 0 || n > PC_DBM_VAL_MAX)
261 {
262 return false; // too large for the L2 value bound -> stays L1-only
263 }
264 return pc_dbm_put(db, (const char *)e->digest, 32, scratch, (uint32_t)n);
265}
266
267bool edge_sd_get(pc_dbm *db, const uint8_t digest[32], EdgeEntry *e, uint8_t *scratch, size_t scratch_cap)
268{
269 if (!db || !digest || !e || !scratch)
270 {
271 return false;
272 }
273 long n = pc_dbm_get(db, (const char *)digest, 32, scratch, scratch_cap);
274 if (n < 0)
275 {
276 return false;
277 }
278 return edge_sd_deserialize(scratch, (size_t)n, e);
279}
280
281bool edge_sd_del(pc_dbm *db, const uint8_t digest[32])
282{
283 return db && digest && pc_dbm_del(db, (const char *)digest, 32);
284}
285
286uint32_t edge_sd_purge_prefix(pc_dbm *db, const char *path_prefix, uint8_t *scratch, size_t scratch_cap)
287{
288 if (!db || !path_prefix || !scratch)
289 {
290 return 0;
291 }
292 return purge_matching(db, path_prefix, scratch, scratch_cap);
293}
294
295uint32_t edge_sd_purge_all(pc_dbm *db)
296{
297 if (!db)
298 {
299 return 0;
300 }
301 // purge_all still verifies each value is an edge serialization before deleting, so a shared dbm is safe;
302 // that needs a scratch buffer to read each value into.
303 uint8_t scratch[PC_EDGE_SD_VALUE_MAX];
304 return purge_matching(db, nullptr, scratch, sizeof(scratch));
305}
306
307#endif // PC_ENABLE_DBM
308
309#endif // PC_ENABLE_EDGE_CACHE
#define PC_EDGE_BODY_MAX
CDN edge-cache tier - L2 SD persistence (PC_ENABLE_EDGE_CACHE && PC_ENABLE_DBM).
#define PC_EDGE_KEY_MAX
#define PC_DBM_KEY_MAX
#define PC_EDGE_SD_VALUE_MAX
Worst-case serialized L2 entry (edge_sd_serialize).
#define PC_DBM_VAL_MAX