DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
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 DETWS_ENABLE_EDGE_CACHE && DETWS_ENABLE_DBM
12
13#include <string.h>
14
15// The L2 key is the entry's SHA-256 digest, which must fit a dbm key exactly.
16static_assert(DETWS_DBM_KEY_MAX >= 32, "edge cache L2 uses a 32-byte SHA-256 digest as the dbm key");
17
18namespace
19{
20constexpr uint8_t EDGE_SD_VERSION = 1;
21
22void put_u16(uint8_t *p, uint16_t v)
23{
24 p[0] = (uint8_t)(v & 0xFF);
25 p[1] = (uint8_t)(v >> 8);
26}
27uint16_t get_u16(const uint8_t *p)
28{
29 return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
30}
31
32// Append a u16 length prefix + the NUL-terminated string @p s. False (no write) on overflow.
33bool put_str(uint8_t *out, size_t cap, size_t *pos, const char *s)
34{
35 size_t sl = strlen(s);
36 if (sl > 0xFFFFu || *pos + 2 + sl > cap)
37 return false;
38 put_u16(out + *pos, (uint16_t)sl);
39 *pos += 2;
40 memcpy(out + *pos, s, sl);
41 *pos += sl;
42 return true;
43}
44
45// Read a u16-prefixed string into @p out (NUL-terminated). False if short or it would not fit (no truncation).
46bool get_str(const uint8_t *buf, size_t len, size_t *pos, char *out, size_t out_cap)
47{
48 if (*pos + 2 > len)
49 return false;
50 uint16_t sl = get_u16(buf + *pos);
51 *pos += 2;
52 if (*pos + sl > len || sl >= out_cap)
53 return false;
54 memcpy(out, buf + *pos, sl);
55 out[sl] = '\0';
56 *pos += sl;
57 return true;
58}
59
60// The path portion of a canonical key "METHOD\nhost\npath[\nquery]" (after the 2nd '\n'), or nullptr.
61const char *canon_path(const char *canon)
62{
63 int nl = 0;
64 for (const char *p = canon; *p; p++)
65 if (*p == '\n' && ++nl == 2)
66 return p + 1;
67 return nullptr;
68}
69
70// True if @p buf is a valid edge serialization; if so copies its canonical key into @p canon_out.
71bool peek_canon(const uint8_t *buf, size_t len, char *canon_out, size_t cap)
72{
73 if (len < 3 || buf[0] != EDGE_SD_VERSION)
74 return false;
75 size_t pos = 3; // version(1) + status(2)
76 return get_str(buf, len, &pos, canon_out, cap);
77}
78
79// Batch of L2 keys collected during an iteration for deletion afterward (dbm forbids delete-in-iterate).
80constexpr int EDGE_SD_PURGE_BATCH = 8;
81struct CollectCtx
82{
83 DetwsDbm *db;
84 const char *prefix; // nullptr = match every edge entry
85 size_t plen;
86 uint8_t *scratch;
87 size_t scratch_cap;
88 uint8_t batch[EDGE_SD_PURGE_BATCH][32];
89 int count;
90 bool full; // hit the batch cap: another pass is needed
91};
92
93bool collect_cb(const char *key, uint16_t key_len, void *vctx)
94{
95 CollectCtx *c = (CollectCtx *)vctx;
96 if (key_len != 32)
97 return true; // not an edge digest key (shared dbm) - leave it be
98 long n = detws_dbm_get(c->db, key, key_len, c->scratch, c->scratch_cap);
99 if (n <= 0)
100 return true;
101 char canon[DETWS_EDGE_KEY_MAX];
102 if (!peek_canon(c->scratch, (size_t)n, canon, sizeof(canon)))
103 return true; // not an edge value - do not touch it
104 if (c->prefix)
105 {
106 const char *path = canon_path(canon);
107 if (!path || strncmp(path, c->prefix, c->plen) != 0)
108 return true; // path does not match the purge prefix
109 }
110 if (c->count >= EDGE_SD_PURGE_BATCH)
111 {
112 c->full = true;
113 return false; // stop this pass; the caller deletes the batch then re-iterates
114 }
115 memcpy(c->batch[c->count++], key, 32);
116 return true;
117}
118
119uint32_t purge_matching(DetwsDbm *db, const char *prefix, uint8_t *scratch, size_t scratch_cap)
120{
121 uint32_t total = 0;
122 for (;;)
123 {
124 CollectCtx c;
125 c.db = db;
126 c.prefix = prefix;
127 c.plen = prefix ? strlen(prefix) : 0;
128 c.scratch = scratch;
129 c.scratch_cap = scratch_cap;
130 c.count = 0;
131 c.full = false;
132 detws_dbm_iterate(db, collect_cb, &c);
133 for (int i = 0; i < c.count; i++)
134 if (detws_dbm_del(db, (const char *)c.batch[i], 32))
135 total++;
136 if (!c.full)
137 break; // visited everything that matched
138 }
139 return total;
140}
141} // namespace
142
143size_t edge_sd_serialize(const EdgeEntry *e, uint8_t *out, size_t cap)
144{
145 if (!e || !out || cap < 3)
146 return 0;
147 size_t pos = 0;
148 out[pos++] = EDGE_SD_VERSION;
149 put_u16(out + pos, (uint16_t)e->status);
150 pos += 2;
151 if (!put_str(out, cap, &pos, e->key) || !put_str(out, cap, &pos, e->content_type) ||
152 !put_str(out, cap, &pos, e->etag) || !put_str(out, cap, &pos, e->last_modified) ||
153 !put_str(out, cap, &pos, e->content_encoding) || !put_str(out, cap, &pos, e->vary_names) ||
154 !put_str(out, cap, &pos, e->vary_vals))
155 return 0;
156 if (pos + 2 + e->body_len > cap)
157 return 0;
158 put_u16(out + pos, e->body_len);
159 pos += 2;
160 memcpy(out + pos, e->body, e->body_len);
161 pos += e->body_len;
162 return pos;
163}
164
165bool edge_sd_deserialize(const uint8_t *buf, size_t len, EdgeEntry *e)
166{
167 if (!buf || !e || len < 3 || buf[0] != EDGE_SD_VERSION)
168 return false;
169 size_t pos = 1;
170 e->status = get_u16(buf + pos);
171 pos += 2;
172 if (!get_str(buf, len, &pos, e->key, sizeof(e->key)) ||
173 !get_str(buf, len, &pos, e->content_type, sizeof(e->content_type)) ||
174 !get_str(buf, len, &pos, e->etag, sizeof(e->etag)) ||
175 !get_str(buf, len, &pos, e->last_modified, sizeof(e->last_modified)) ||
176 !get_str(buf, len, &pos, e->content_encoding, sizeof(e->content_encoding)) ||
177 !get_str(buf, len, &pos, e->vary_names, sizeof(e->vary_names)) ||
178 !get_str(buf, len, &pos, e->vary_vals, sizeof(e->vary_vals)))
179 return false;
180 if (pos + 2 > len)
181 return false;
182 uint16_t bl = get_u16(buf + pos);
183 pos += 2;
184 if (bl > DETWS_EDGE_BODY_MAX || pos + bl > len)
185 return false;
186 memcpy(e->body, buf + pos, bl);
187 e->body_len = bl;
188 edge_key_digest(e->key, strlen(e->key), e->digest); // re-derive the digest from the restored key
189 return true;
190}
191
192bool edge_sd_put(DetwsDbm *db, const EdgeEntry *e, uint8_t *scratch, size_t scratch_cap)
193{
194 if (!db || !e || !scratch)
195 return false;
196 if (!edge_entry_has_validator(e))
197 return false; // only spill what a cheap 304 can refresh after a reboot
198 size_t n = edge_sd_serialize(e, scratch, scratch_cap);
199 if (n == 0 || n > DETWS_DBM_VAL_MAX)
200 return false; // too large for the L2 value bound -> stays L1-only
201 return detws_dbm_put(db, (const char *)e->digest, 32, scratch, (uint32_t)n);
202}
203
204bool edge_sd_get(DetwsDbm *db, const uint8_t digest[32], EdgeEntry *e, uint8_t *scratch, size_t scratch_cap)
205{
206 if (!db || !digest || !e || !scratch)
207 return false;
208 long n = detws_dbm_get(db, (const char *)digest, 32, scratch, scratch_cap);
209 if (n < 0)
210 return false;
211 return edge_sd_deserialize(scratch, (size_t)n, e);
212}
213
214bool edge_sd_del(DetwsDbm *db, const uint8_t digest[32])
215{
216 return db && digest && detws_dbm_del(db, (const char *)digest, 32);
217}
218
219uint32_t edge_sd_purge_prefix(DetwsDbm *db, const char *path_prefix, uint8_t *scratch, size_t scratch_cap)
220{
221 if (!db || !path_prefix || !scratch)
222 return 0;
223 return purge_matching(db, path_prefix, scratch, scratch_cap);
224}
225
226uint32_t edge_sd_purge_all(DetwsDbm *db)
227{
228 if (!db)
229 return 0;
230 // purge_all still verifies each value is an edge serialization before deleting, so a shared dbm is safe;
231 // that needs a scratch buffer to read each value into.
232 uint8_t scratch[EDGE_SD_VALUE_MAX];
233 return purge_matching(db, nullptr, scratch, sizeof(scratch));
234}
235
236#endif // DETWS_ENABLE_EDGE_CACHE && DETWS_ENABLE_DBM
#define DETWS_EDGE_BODY_MAX
#define DETWS_DBM_VAL_MAX
#define DETWS_DBM_KEY_MAX
#define DETWS_EDGE_KEY_MAX
CDN edge-cache tier - L2 SD persistence (DETWS_ENABLE_EDGE_CACHE && DETWS_ENABLE_DBM).