ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
wal_store.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 pc_wal_store.cpp
6 * @brief A/B superblock + checkpoint + mount/recover over a block-device seam (see pc_wal_store.h).
7 */
8
10
11#if PC_ENABLE_WAL
12
13#include <string.h>
14
16
17namespace
18{
19// SUPER_USED bytes are CRC-covered; the u32 CRC follows immediately.
20const size_t SUPER_USED = 28;
21
22// Exact-length device I/O: any short move is a failure.
23bool dev_read(const WalDev &d, uint64_t off, uint8_t *buf, size_t len)
24{
25 return d.read && d.read(d.ctx, off, buf, len) == len;
26}
27bool dev_write(const WalDev &d, uint64_t off, const uint8_t *buf, size_t len)
28{
29 return d.write && d.write(d.ctx, off, buf, len) == len;
30}
31
32// Encode a superblock into a WAL_SUPER_SIZE scratch and write it to copy `ab` (0 or 1).
33bool write_super(WalStore *s, int ab, uint64_t gen, uint64_t head, uint64_t seq)
34{
35 uint8_t sb[WAL_SUPER_SIZE];
36 memset(sb, 0, sizeof(sb));
37 pc_wr32le(sb + 0, WAL_SUPER_MAGIC);
38 pc_wr64le(sb + 4, gen);
39 pc_wr64le(sb + 12, head);
40 pc_wr64le(sb + 20, seq);
41 pc_wr32le(sb + 28, pc_wal_crc32(sb, SUPER_USED));
42 return dev_write(s->dev, (uint64_t)ab * WAL_SUPER_SIZE, sb, sizeof(sb));
43}
44
45// Read + validate a superblock copy. On a good CRC+magic fills gen/head/seq and returns true.
46bool read_super(const WalStore *s, int ab, uint64_t &gen, uint64_t &head, uint64_t &seq)
47{
48 uint8_t sb[WAL_SUPER_SIZE];
49 if (!dev_read(s->dev, (uint64_t)ab * WAL_SUPER_SIZE, sb, sizeof(sb)))
50 {
51 return false;
52 }
53 if (pc_rd32le(sb + 0) != WAL_SUPER_MAGIC)
54 {
55 return false;
56 }
57 if (pc_rd32le(sb + 28) != pc_wal_crc32(sb, SUPER_USED))
58 {
59 return false;
60 }
61 gen = pc_rd64le(sb + 4);
62 head = pc_rd64le(sb + 12);
63 seq = pc_rd64le(sb + 20);
64 // A head past the data region is corruption a matching CRC cannot happen for, but guard anyway.
65 if (head > s->data_cap)
66 {
67 return false;
68 }
69 return true;
70}
71
72// Replay records appended after the committed head: each is CRC self-validating, so recover them one by
73// one and stop at the first torn / short / non-record. Advances s->head and s->next_seq.
74void pc_wal_replay_tail(WalStore *s)
75{
76 uint8_t hdr[WAL_RECORD_HEADER];
77 uint8_t chunk[256];
78 for (;;)
79 {
80 uint64_t off = s->head;
81 if (off + WAL_RECORD_HEADER > s->data_cap)
82 {
83 break;
84 }
85 if (!dev_read(s->dev, s->data_off + off, hdr, WAL_RECORD_HEADER))
86 {
87 break;
88 }
89 if (pc_rd32le(hdr + 0) != WAL_MAGIC)
90 {
91 break;
92 }
93 uint64_t seq = pc_rd64le(hdr + 4);
94 uint32_t plen = pc_rd32le(hdr + 12);
95 uint32_t crc_stored = pc_rd32le(hdr + 16);
96 if (off + (uint64_t)WAL_RECORD_HEADER + plen > s->data_cap)
97 {
98 break; // truncated tail
99 }
100 // CRC over the 16 header bytes then the payload, streamed in small chunks.
101 uint32_t crc = pc_wal_crc32_update(pc_wal_crc32_init(), hdr, 16);
102 uint64_t pos = s->data_off + off + WAL_RECORD_HEADER;
103 uint32_t left = plen;
104 bool read_ok = true;
105 while (left)
106 {
107 size_t n = left < sizeof(chunk) ? left : sizeof(chunk);
108 if (!dev_read(s->dev, pos, chunk, n))
109 {
110 read_ok = false;
111 break;
112 }
113 crc = pc_wal_crc32_update(crc, chunk, n);
114 pos += n;
115 left -= (uint32_t)n;
116 }
117 if (!read_ok || pc_wal_crc32_final(crc) != crc_stored)
118 {
119 break; // torn / corrupt record - this is the durable end
120 }
121 s->head = off + (uint64_t)WAL_RECORD_HEADER + plen;
122 if (seq + 1 > s->next_seq)
123 {
124 s->next_seq = seq + 1;
125 }
126 }
127}
128} // namespace
129
130bool pc_wal_store_format(WalStore *s, const WalDev *dev)
131{
132 if (!dev || dev->size <= WAL_DATA_OFFSET)
133 {
134 return false;
135 }
136 memset(s, 0, sizeof(*s));
137 s->dev = *dev;
138 s->data_off = WAL_DATA_OFFSET;
139 s->data_cap = dev->size - WAL_DATA_OFFSET;
140 s->head = 0;
141 s->committed = 0;
142 s->next_seq = 0;
143 s->gen = 1;
144 s->ab = 0;
145 // Invalidate copy B, then commit copy A as the live generation-1 superblock.
146 uint8_t zero[WAL_SUPER_SIZE];
147 memset(zero, 0, sizeof(zero));
148 if (!dev_write(s->dev, (uint64_t)1 * WAL_SUPER_SIZE, zero, sizeof(zero)))
149 {
150 return false;
151 }
152 if (!write_super(s, 0, 1, 0, 0))
153 {
154 return false;
155 }
156 return s->dev.sync ? s->dev.sync(s->dev.ctx) : true;
157}
158
159bool pc_wal_store_mount(WalStore *s, const WalDev *dev)
160{
161 if (!dev || dev->size <= WAL_DATA_OFFSET)
162 {
163 return false;
164 }
165 memset(s, 0, sizeof(*s));
166 s->dev = *dev;
167 s->data_off = WAL_DATA_OFFSET;
168 s->data_cap = dev->size - WAL_DATA_OFFSET;
169
170 uint64_t genA = 0;
171 uint64_t headA = 0;
172 uint64_t seqA = 0;
173 uint64_t genB = 0;
174 uint64_t headB = 0;
175 uint64_t seqB = 0;
176 bool okA = read_super(s, 0, genA, headA, seqA);
177 bool okB = read_super(s, 1, genB, headB, seqB);
178 if (!okA && !okB)
179 {
180 return false; // unformatted or both torn
181 }
182
183 // Newest valid superblock wins (higher generation).
184 if (okA && (!okB || genA >= genB))
185 {
186 s->ab = 0;
187 s->gen = genA;
188 s->committed = headA;
189 s->next_seq = seqA;
190 }
191 else
192 {
193 s->ab = 1;
194 s->gen = genB;
195 s->committed = headB;
196 s->next_seq = seqB;
197 }
198 s->head = s->committed;
199
200 // Recover any records appended after the committed head (each CRC self-validating).
201 pc_wal_replay_tail(s);
202 return true;
203}
204
205bool pc_wal_store_append(WalStore *s, const uint8_t *payload, uint32_t len)
206{
207 uint64_t need = (uint64_t)WAL_RECORD_HEADER + len;
208 if (s->head + need > s->data_cap)
209 {
210 return false; // log full
211 }
212 // Assemble the 20-byte header (magic+seq+len+crc); CRC covers header + payload without buffering both.
213 uint8_t hdr[WAL_RECORD_HEADER];
214 pc_wr32le(hdr + 0, WAL_MAGIC);
215 pc_wr64le(hdr + 4, s->next_seq);
216 pc_wr32le(hdr + 12, len);
217 uint32_t crc = pc_wal_crc32_update(pc_wal_crc32_init(), hdr, 16);
218 crc = pc_wal_crc32_update(crc, payload, len);
219 pc_wr32le(hdr + 16, pc_wal_crc32_final(crc));
220
221 uint64_t at = s->data_off + s->head;
222 if (!dev_write(s->dev, at, hdr, WAL_RECORD_HEADER))
223 {
224 return false;
225 }
226 if (len && !dev_write(s->dev, at + WAL_RECORD_HEADER, payload, len))
227 {
228 return false;
229 }
230 s->head += need;
231 s->next_seq++;
232 return true;
233}
234
235bool pc_wal_store_checkpoint(WalStore *s)
236{
237 // Data first: the appended records must be durable before the pointer that commits them advances.
238 if (s->dev.sync && !s->dev.sync(s->dev.ctx))
239 {
240 return false;
241 }
242 int target = 1 - s->ab;
243 uint64_t gen = s->gen + 1;
244 if (!write_super(s, target, gen, s->head, s->next_seq))
245 {
246 return false;
247 }
248 if (s->dev.sync && !s->dev.sync(s->dev.ctx))
249 {
250 return false;
251 }
252 // The superblock flip is the commit point.
253 s->gen = gen;
254 s->ab = target;
255 s->committed = s->head;
256 return true;
257}
258
259size_t pc_wal_store_scan(WalStore *s, WalStoreRecordCb cb, void *ctx, uint8_t *scratch, size_t scratch_len)
260{
261 if (scratch_len < WAL_RECORD_HEADER)
262 {
263 return 0;
264 }
265 size_t count = 0;
266 uint64_t off = 0;
267 while (off + WAL_RECORD_HEADER <= s->head)
268 {
269 if (!dev_read(s->dev, s->data_off + off, scratch, WAL_RECORD_HEADER))
270 {
271 break;
272 }
273 if (pc_rd32le(scratch) != WAL_MAGIC)
274 {
275 break;
276 }
277 uint32_t plen = pc_rd32le(scratch + 12);
278 size_t total = (size_t)WAL_RECORD_HEADER + plen;
279 if (off + total > s->head || total > scratch_len)
280 {
281 break; // truncated within the log, or a record too large for the caller's scratch
282 }
283 if (!dev_read(s->dev, s->data_off + off, scratch, total))
284 {
285 break;
286 }
287 uint32_t crc = pc_wal_crc32_update(pc_wal_crc32_init(), scratch, 16);
288 crc = pc_wal_crc32_update(crc, scratch + WAL_RECORD_HEADER, plen);
289 if (pc_wal_crc32_final(crc) != pc_rd32le(scratch + 16))
290 {
291 break;
292 }
293 if (cb)
294 {
295 cb(pc_rd64le(scratch + 4), off, scratch + WAL_RECORD_HEADER, plen, ctx);
296 }
297 count++;
298 off += total;
299 }
300 return count;
301}
302
303bool pc_wal_store_pread(WalStore *s, uint64_t off, uint8_t *buf, size_t len)
304{
305 if (off + len > s->data_cap)
306 {
307 return false;
308 }
309 return dev_read(s->dev, s->data_off + off, buf, len);
310}
311
312#endif // PC_ENABLE_WAL
Fixed-width integer serializers into a raw uint8_t* buffer - one source of truth.
size_t pc_wr64le(uint8_t *p, uint64_t v)
Write v little-endian at p.
Definition endian.h:50
uint64_t pc_rd64le(const uint8_t *p)
Read a little-endian u64 at p.
Definition endian.h:72
uint32_t pc_rd32le(const uint8_t *p)
Read a little-endian u32 at p.
Definition endian.h:66
size_t pc_wr32le(uint8_t *p, uint32_t v)
Write v little-endian at p.
Definition endian.h:40