1use crate::bytesio::{get_u16, get_u32, get_u64, put_u16, put_u32, put_u64};
24use crate::error::{Error, Result};
25use crate::hash::xxh3_64;
26use crate::key::UrlKey;
27
28pub const MAGIC: u32 = 0x5750_5247;
30pub const VERSION: u16 = 3;
33pub const HEADER_LEN: usize = 64;
35pub const MAX_EDGES: usize = 4096;
37pub const MAX_ATTRS: usize = 64;
39pub const MAX_ATTR_LEN: usize = 256 << 10;
41
42const OP_UPSERT: u8 = 1;
43const OP_SEGS: u8 = 2;
44const OP_EDGES: u8 = 3;
45const OP_TOUCH: u8 = 4;
46const OP_REMOVE: u8 = 5;
47const OP_PIN: u8 = 6;
48const OP_ATTRS: u8 = 8;
50
51#[must_use]
53pub fn encode_header(base_generation: u64, created_at_ms: u64) -> Vec<u8> {
54 let mut out = Vec::with_capacity(HEADER_LEN);
55 put_u32(&mut out, MAGIC);
56 put_u16(&mut out, VERSION);
57 put_u16(&mut out, 0);
58 put_u32(&mut out, HEADER_LEN as u32);
59 put_u32(&mut out, 0);
61 put_u64(&mut out, base_generation);
62 put_u64(&mut out, created_at_ms);
63 out.resize(HEADER_LEN - 8, 0);
64 let sum = xxh3_64(&out[..HEADER_LEN - 8]);
65 put_u64(&mut out, sum);
66 out
67}
68
69pub fn parse_header(bytes: &[u8]) -> Result<u64> {
71 if bytes.len() < HEADER_LEN {
72 return Err(Error::format("wal shorter than header"));
73 }
74 if crate::bytesio::get_u32(bytes, 0) != MAGIC {
75 return Err(Error::format("bad wal magic"));
76 }
77 let version = crate::bytesio::get_u16(bytes, 4);
78 if version != VERSION {
79 return Err(Error::Version { found: version, supported: VERSION });
80 }
81 let stored = crate::bytesio::get_u64(bytes, HEADER_LEN - 8);
82 if xxh3_64(&bytes[..HEADER_LEN - 8]) != stored {
83 return Err(Error::format("wal header checksum mismatch"));
84 }
85 Ok(crate::bytesio::get_u64(bytes, 16))
86}
87
88pub const RECORD_HEADER_LEN: usize = 32;
105pub const OP_HEADER_LEN: usize = 16;
107pub const MAX_RECORD: usize = 256 << 20;
110pub const MAX_LABEL: usize = 2048;
112pub const MAX_SEGS: usize = 128;
114pub const MAX_OP: usize = 1_048_246 + ATTRS_FIXED + MAX_ATTRS * ATTR_ENTRY;
117
118const TAG: u16 = 0x5854; const COMMIT: u8 = 0x01;
120const MIN_RECORD: usize = RECORD_HEADER_LEN + OP_HEADER_LEN;
121
122const UPSERT_FIXED: usize = OP_HEADER_LEN + 32;
123const SEGS_FIXED: usize = OP_HEADER_LEN + 8;
124const SEG_ENTRY: usize = 16;
125const EDGES_FIXED: usize = OP_HEADER_LEN + 8;
126const EDGE_ENTRY: usize = 12;
127const TOUCH_FIXED: usize = OP_HEADER_LEN + 40;
128const ATTRS_FIXED: usize = OP_HEADER_LEN + 8;
129const ATTR_ENTRY: usize = 8;
130
131const HAS_TITLE: u8 = 1;
133const HAS_SNIPPET: u8 = 2;
134const HAS_ETAG: u8 = 4;
135const T_HASH: u8 = 1;
137const T_ETAG: u8 = 2;
138const T_TOMBSTONE: u8 = 4;
139
140pub type Edge = (UrlKey, u8, u8, u16);
142
143fn record_sum(rec: &[u8]) -> u64 {
146 let mut h = crate::hash::Hasher::new();
147 h.update(&rec[..24]);
148 h.update(&rec[RECORD_HEADER_LEN..]);
149 h.digest64()
150}
151
152#[derive(Clone, Copy, Debug)]
155pub struct TouchRecord<'a> {
156 pub checked_at_ms: u64,
158 pub outcome: u8,
160 pub content_hash: Option<u64>,
162 pub etag: Option<&'a str>,
164 pub interval_s: u32,
166 pub checks: u16,
168 pub changes: u16,
170 pub last_change_ms: u64,
172 pub tombstone: bool,
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178pub struct Mark {
179 len: usize,
180 ops: u32,
181}
182
183#[derive(Debug, Default)]
188pub struct RecordBuf {
189 buf: Vec<u8>,
190 ops: u32,
191}
192
193impl RecordBuf {
194 #[must_use]
196 pub fn new() -> Self {
197 Self::default()
198 }
199
200 #[must_use]
202 pub fn ops(&self) -> u32 {
203 self.ops
204 }
205
206 #[must_use]
208 pub fn is_empty(&self) -> bool {
209 self.ops == 0
210 }
211
212 #[must_use]
214 pub fn len(&self) -> usize {
215 self.buf.len()
216 }
217
218 #[must_use]
220 pub fn mark(&self) -> Mark {
221 Mark { len: self.buf.len(), ops: self.ops }
222 }
223
224 pub fn truncate(&mut self, mark: Mark) {
226 if mark.ops == 0 {
227 self.buf.clear();
228 self.ops = 0;
229 } else if mark.len <= self.buf.len() && mark.ops <= self.ops {
230 self.buf.truncate(mark.len);
231 self.ops = mark.ops;
232 }
233 }
234
235 #[must_use]
237 pub fn as_bytes(&self) -> &[u8] {
238 &self.buf
239 }
240
241 #[must_use]
243 pub fn into_vec(self) -> Vec<u8> {
244 self.buf
245 }
246
247 pub fn op_views(&self) -> impl Iterator<Item = OpView<'_>> + '_ {
249 OpWalk { bytes: &self.buf, pos: RECORD_HEADER_LEN.min(self.buf.len()) }
250 .filter_map(|(_, op)| OpView::trusted(op))
251 }
252
253 fn begin(&mut self, kind: u8, oflags: u8, key: UrlKey, body: usize) -> usize {
256 if self.buf.is_empty() {
257 self.buf.reserve(RECORD_HEADER_LEN + OP_HEADER_LEN + body);
258 self.buf.resize(RECORD_HEADER_LEN, 0);
259 }
260 let at = self.buf.len();
261 self.buf.reserve(OP_HEADER_LEN + body);
262 put_u32(&mut self.buf, 0);
263 self.buf.extend_from_slice(&[kind, oflags, 0, 0]);
264 put_u64(&mut self.buf, key.0);
265 at
266 }
267
268 fn finish(&mut self, at: usize) {
269 let len = (self.buf.len() - at) as u32;
270 self.buf[at..at + 4].copy_from_slice(&len.to_le_bytes());
271 self.ops += 1;
272 }
273
274 pub fn push_upsert(&mut self, doc: &crate::writer::DocRecord<'_>, flags: u16) -> Result<()> {
276 if doc.url.is_empty() || doc.url.len() > MAX_LABEL {
277 return Err(Error::format(format!("url must be 1..={MAX_LABEL} bytes")));
278 }
279 let opt = [doc.title, doc.snippet, doc.etag];
280 if opt.iter().flatten().any(|s| s.len() > MAX_LABEL) {
281 return Err(Error::format(format!("a label exceeds {MAX_LABEL} bytes")));
282 }
283 let text = doc.url.len() + opt.iter().flatten().map(|s| s.len()).sum::<usize>();
284 let present = opt.iter().zip([HAS_TITLE, HAS_SNIPPET, HAS_ETAG]).fold(0, |p, (s, bit)| {
285 if s.is_some() {
286 p | bit
287 } else {
288 p
289 }
290 });
291 let at = self.begin(OP_UPSERT, 0, doc.url_key, 32 + text);
292 put_u64(&mut self.buf, doc.content_hash);
293 put_u64(&mut self.buf, doc.fetched_at_ms);
294 put_u16(&mut self.buf, flags);
295 self.buf.extend_from_slice(&[present, 0]);
296 put_u16(&mut self.buf, doc.url.len() as u16);
297 for s in opt {
298 put_u16(&mut self.buf, s.map_or(0, |s| s.len() as u16));
299 }
300 put_u32(&mut self.buf, 0);
301 self.buf.extend_from_slice(doc.url.as_bytes());
302 for s in opt.into_iter().flatten() {
303 self.buf.extend_from_slice(s.as_bytes());
304 }
305 self.finish(at);
306 Ok(())
307 }
308
309 pub fn push_segments(
312 &mut self,
313 key: UrlKey,
314 segs: &[crate::writer::SegmentRecord<'_>],
315 ) -> Result<()> {
316 if segs.len() > MAX_SEGS {
317 return Err(Error::format(format!("more than {MAX_SEGS} segments")));
318 }
319 if segs.iter().any(|s| s.label.len() > MAX_LABEL) {
320 return Err(Error::format(format!("segment label exceeds {MAX_LABEL} bytes")));
321 }
322 let text: usize = segs.iter().map(|s| s.label.len()).sum();
323 let at = self.begin(OP_SEGS, 0, key, 8 + segs.len() * SEG_ENTRY + text);
324 self.buf.extend_from_slice(&[segs.len() as u8, 0, 0, 0, 0, 0, 0, 0]);
325 for s in segs {
326 let (start, len, flags) = match s.byte_range {
327 Some((start, len)) => (start, len, 0),
328 None => (0, 0, crate::format::base::sflags::NO_RANGE),
329 };
330 put_u32(&mut self.buf, start);
331 put_u32(&mut self.buf, len);
332 put_u16(&mut self.buf, s.importance);
333 put_u16(&mut self.buf, s.label.len() as u16);
334 self.buf.extend_from_slice(&[s.depth.max(1), flags, 0, 0]);
335 }
336 for s in segs {
337 self.buf.extend_from_slice(s.label.as_bytes());
338 }
339 self.finish(at);
340 Ok(())
341 }
342
343 pub fn push_edges(&mut self, key: UrlKey, edges: &[Edge]) -> Result<()> {
346 if edges.len() > MAX_EDGES {
347 return Err(Error::format(format!("more than {MAX_EDGES} edges")));
348 }
349 if edges.windows(2).any(|w| w[0].0 .0 >= w[1].0 .0) {
350 return Err(Error::format("edge destinations must be strictly ascending"));
351 }
352 let at = self.begin(OP_EDGES, 0, key, 8 + edges.len() * EDGE_ENTRY);
353 put_u16(&mut self.buf, edges.len() as u16);
354 self.buf.extend_from_slice(&[0; 6]);
355 for &(dst, etype, eflags, weight) in edges {
356 put_u64(&mut self.buf, dst.0);
357 self.buf.extend_from_slice(&[etype, eflags]);
358 put_u16(&mut self.buf, weight);
359 }
360 self.finish(at);
361 Ok(())
362 }
363
364 pub fn push_touch(&mut self, key: UrlKey, t: &TouchRecord<'_>) -> Result<()> {
366 let etag = t.etag.unwrap_or("");
367 if etag.len() > MAX_LABEL {
368 return Err(Error::format(format!("etag exceeds {MAX_LABEL} bytes")));
369 }
370 let at = self.begin(OP_TOUCH, 0, key, 40 + etag.len());
371 put_u64(&mut self.buf, t.checked_at_ms);
372 put_u64(&mut self.buf, t.content_hash.unwrap_or(0));
373 put_u64(&mut self.buf, t.last_change_ms);
374 put_u32(&mut self.buf, t.interval_s);
375 put_u16(&mut self.buf, t.checks);
376 put_u16(&mut self.buf, t.changes);
377 let bits = (u8::from(t.content_hash.is_some()) * T_HASH)
378 | (u8::from(t.etag.is_some()) * T_ETAG)
379 | (u8::from(t.tombstone) * T_TOMBSTONE);
380 self.buf.extend_from_slice(&[t.outcome, bits]);
381 put_u16(&mut self.buf, etag.len() as u16);
382 put_u32(&mut self.buf, 0);
383 self.buf.extend_from_slice(etag.as_bytes());
384 self.finish(at);
385 Ok(())
386 }
387
388 pub fn push_remove(&mut self, key: UrlKey) {
390 let at = self.begin(OP_REMOVE, 0, key, 0);
391 self.finish(at);
392 }
393
394 pub fn push_pin(&mut self, key: UrlKey, pinned: bool) {
396 let at = self.begin(OP_PIN, u8::from(pinned), key, 0);
397 self.finish(at);
398 }
399
400 pub fn push_attrs(&mut self, key: UrlKey, attrs: &[(u8, &str)]) -> Result<()> {
404 let parts: Vec<(u8, [&str; 3])> = attrs.iter().map(|&(k, v)| (k, [v, "", ""])).collect();
405 self.push_attr_parts(key, &parts)
406 }
407
408 pub(crate) fn push_attr_parts(&mut self, key: UrlKey, attrs: &[(u8, [&str; 3])]) -> Result<()> {
412 let n = attrs.len();
413 if n > MAX_ATTRS {
414 return Err(Error::format(format!("more than {MAX_ATTRS} attributes")));
415 }
416 let mut total = 0usize;
417 let mut prev: Option<u8> = None;
418 for &(k, parts) in attrs {
419 if prev.is_some_and(|p| p >= k) {
420 return Err(Error::format("attribute keys must be strictly ascending"));
421 }
422 prev = Some(k);
423 let len: usize = parts.iter().map(|p| p.len()).sum();
424 if len > MAX_ATTR_LEN {
425 return Err(Error::format("attribute too long"));
426 }
427 total += len;
428 }
429 if ATTRS_FIXED + n * ATTR_ENTRY + total > MAX_OP {
430 return Err(Error::format(format!("attributes exceed {MAX_OP} bytes in one op")));
431 }
432 let at = self.begin(OP_ATTRS, 0, key, 8 + n * ATTR_ENTRY + total);
433 self.buf.extend_from_slice(&[n as u8, 0, 0, 0, 0, 0, 0, 0]);
434 for &(k, parts) in attrs {
435 self.buf.extend_from_slice(&[k, 0, 0, 0]);
436 put_u32(&mut self.buf, parts.iter().map(|p| p.len() as u32).sum());
437 }
438 for (_, parts) in attrs {
439 for p in parts {
440 self.buf.extend_from_slice(p.as_bytes());
441 }
442 }
443 self.finish(at);
444 Ok(())
445 }
446
447 pub fn seal(&mut self, first_seq: u64) -> Result<()> {
453 if self.ops == 0 {
454 return Err(Error::format("a record needs at least one op"));
455 }
456 if self.buf.len() > MAX_RECORD {
457 return Err(Error::format(format!(
458 "a transaction of {} bytes exceeds the {MAX_RECORD}-byte record limit; \
459 commit it in parts",
460 self.buf.len()
461 )));
462 }
463 if first_seq == 0 || first_seq.checked_add(u64::from(self.ops) - 1).is_none() {
464 return Err(Error::format("record sequence numbers out of range"));
465 }
466 let len = self.buf.len() as u32;
467 let head = &mut self.buf[..RECORD_HEADER_LEN];
468 head[0..4].copy_from_slice(&len.to_le_bytes());
469 head[4..6].copy_from_slice(&TAG.to_le_bytes());
470 head[6..8].copy_from_slice(&[COMMIT, 0]);
471 head[8..16].copy_from_slice(&first_seq.to_le_bytes());
472 head[16..20].copy_from_slice(&self.ops.to_le_bytes());
473 head[20..24].copy_from_slice(&[0; 4]);
474 let sum = record_sum(&self.buf);
475 self.buf[24..32].copy_from_slice(&sum.to_le_bytes());
476 Ok(())
477 }
478}
479
480struct OpWalk<'a> {
483 bytes: &'a [u8],
484 pos: usize,
485}
486
487impl<'a> Iterator for OpWalk<'a> {
488 type Item = (usize, &'a [u8]);
489
490 #[inline]
491 fn next(&mut self) -> Option<Self::Item> {
492 let rest = self.bytes.get(self.pos..)?;
493 if rest.len() < OP_HEADER_LEN {
494 return None;
495 }
496 let len = get_u32(rest, 0) as usize;
497 let op = rest.get(..len).filter(|_| len >= OP_HEADER_LEN)?;
498 let at = self.pos;
499 self.pos += len;
500 Some((at, op))
501 }
502}
503
504#[derive(Clone, Copy, Debug)]
506pub enum Next<'a> {
507 Record(Record<'a>),
509 Torn,
512}
513
514pub fn next_record(bytes: &[u8], pos: usize) -> Result<Next<'_>> {
519 let Some(rest) = bytes.get(pos..) else { return Ok(Next::Torn) };
520 if rest.len() < RECORD_HEADER_LEN {
521 return Ok(Next::Torn);
522 }
523 let len = get_u32(rest, 0) as usize;
524 if !(MIN_RECORD..=MAX_RECORD).contains(&len)
525 || len > rest.len()
526 || get_u16(rest, 4) != TAG
527 || rest[6] != COMMIT
528 {
529 return Ok(Next::Torn);
530 }
531 let rec = &rest[..len];
532 if record_sum(rec) != get_u64(rec, 24) {
533 return Ok(Next::Torn);
534 }
535 let bad = |what: &str| Error::corrupt(format!("wal record at byte {pos}: {what}"));
536 if rec[7] != 0 || get_u32(rec, 20) != 0 {
537 return Err(bad("non-zero reserved header bytes"));
538 }
539 let first_seq = get_u64(rec, 8);
540 let count = get_u32(rec, 16);
541 if first_seq == 0 || count == 0 || first_seq.checked_add(u64::from(count) - 1).is_none() {
542 return Err(bad("sequence numbers out of range"));
543 }
544 let mut at = RECORD_HEADER_LEN;
545 for i in 0..count {
546 if len - at < OP_HEADER_LEN {
547 return Err(bad(&format!("op {i} of {count} missing")));
548 }
549 let op_len = get_u32(rec, at) as usize;
550 if !(OP_HEADER_LEN..=MAX_OP).contains(&op_len) || op_len > len - at {
551 return Err(bad(&format!("op {i} length {op_len} out of range")));
552 }
553 validate_op(&rec[at..at + op_len])
554 .map_err(|e| bad(&format!("op {i} at byte {}: {e}", pos + at)))?;
555 at += op_len;
556 }
557 if at != len {
558 return Err(bad("bytes after the last op"));
559 }
560 Ok(Next::Record(Record { rec, at: pos }))
561}
562
563#[derive(Clone, Copy, Debug)]
565pub struct Record<'a> {
566 rec: &'a [u8],
567 at: usize,
568}
569
570impl<'a> Record<'a> {
571 #[must_use]
573 #[inline]
574 pub fn first_seq(&self) -> u64 {
575 get_u64(self.rec, 8)
576 }
577
578 #[must_use]
580 #[inline]
581 pub fn op_count(&self) -> u32 {
582 get_u32(self.rec, 16)
583 }
584
585 #[must_use]
587 #[inline]
588 pub fn last_seq(&self) -> u64 {
589 self.first_seq() + u64::from(self.op_count()) - 1
590 }
591
592 #[must_use]
594 pub fn len(&self) -> usize {
595 self.rec.len()
596 }
597
598 #[must_use]
600 pub fn is_empty(&self) -> bool {
601 false
602 }
603
604 #[must_use]
606 pub fn offset(&self) -> usize {
607 self.at
608 }
609
610 #[must_use]
612 pub fn bytes(&self) -> &'a [u8] {
613 self.rec
614 }
615
616 pub fn ops(&self) -> impl Iterator<Item = (u64, usize, OpView<'a>)> + 'a {
619 let (first, at) = (self.first_seq(), self.at);
620 OpWalk { bytes: self.rec, pos: RECORD_HEADER_LEN }
621 .zip(0..u64::from(self.op_count()))
622 .filter_map(move |((off, op), i)| OpView::trusted(op).map(|v| (first + i, at + off, v)))
623 }
624}
625
626#[derive(Clone, Debug)]
629pub struct Records<'a> {
630 image: &'a [u8],
631 pos: usize,
632 end: usize,
633}
634
635impl<'a> Records<'a> {
636 pub(crate) fn trusted(image: &'a [u8], pos: usize, end: usize) -> Self {
640 Self { image, pos, end: end.min(image.len()) }
641 }
642}
643
644impl<'a> Iterator for Records<'a> {
645 type Item = Record<'a>;
646
647 #[inline]
648 fn next(&mut self) -> Option<Record<'a>> {
649 if self.end - self.pos.min(self.end) < MIN_RECORD {
650 return None;
651 }
652 let len = get_u32(self.image, self.pos) as usize;
653 let rec = self
654 .image
655 .get(self.pos..self.pos + len)
656 .filter(|_| len >= MIN_RECORD && self.pos + len <= self.end)?;
657 let at = self.pos;
658 self.pos += len;
659 Some(Record { rec, at })
660 }
661}
662
663pub(crate) struct Scan {
668 pub(crate) end: usize,
669 pub(crate) last_seq: Option<u64>,
670 pub(crate) ops: usize,
671 pub(crate) corrupt: Option<Error>,
672}
673
674pub(crate) fn scan(bytes: &[u8], mut pos: usize, after: Option<u64>) -> Scan {
675 let mut out = Scan { end: pos, last_seq: after, ops: 0, corrupt: None };
676 loop {
677 match next_record(bytes, pos) {
678 Ok(Next::Record(r)) => {
679 if out.last_seq.is_some_and(|l| r.first_seq() <= l) {
680 out.corrupt = Some(Error::corrupt(format!(
681 "wal record at byte {pos}: sequence regression"
682 )));
683 return out;
684 }
685 out.last_seq = Some(r.last_seq());
686 out.ops += r.op_count() as usize;
687 pos += r.len();
688 out.end = pos;
689 }
690 Ok(Next::Torn) => return out,
691 Err(e) => {
692 out.corrupt = Some(e);
693 return out;
694 }
695 }
696 }
697}
698
699#[derive(Clone, Debug)]
703pub struct Replay<'a> {
704 pub base_generation: u64,
706 pub good_len: usize,
709 image: &'a [u8],
710}
711
712impl<'a> Replay<'a> {
713 #[must_use]
715 pub fn records(&self) -> Records<'a> {
716 Records::trusted(self.image, HEADER_LEN, self.good_len)
717 }
718}
719
720pub fn replay(image: &[u8]) -> Result<Replay<'_>> {
725 let base_generation = parse_header(image)?;
726 let scanned = scan(image, HEADER_LEN, None);
727 if let Some(e) = scanned.corrupt {
728 return Err(e);
729 }
730 Ok(Replay { base_generation, good_len: scanned.end, image })
731}
732
733#[must_use]
739pub fn committed_len(image: &[u8]) -> usize {
740 if parse_header(image).is_err() {
741 return 0;
742 }
743 HEADER_LEN + committed_frames_len(&image[HEADER_LEN..])
744}
745
746#[must_use]
750pub fn committed_frames_len(frames: &[u8]) -> usize {
751 scan(frames, 0, None).end
752}
753
754#[derive(Clone, Copy, Debug)]
756pub enum OpView<'a> {
757 Upsert(UpsertView<'a>),
759 Segs(SegsView<'a>),
761 Edges(EdgesView<'a>),
763 Touch(TouchView<'a>),
765 Remove(UrlKey),
767 Pin(UrlKey, bool),
769 Attrs(AttrsView<'a>),
771}
772
773impl<'a> OpView<'a> {
774 #[inline]
777 pub(crate) fn trusted(op: &'a [u8]) -> Option<Self> {
778 if op.len() < OP_HEADER_LEN {
779 return None;
780 }
781 let key = UrlKey(get_u64(op, 8));
782 Some(match op[4] {
783 OP_UPSERT => Self::Upsert(UpsertView { op }),
784 OP_SEGS => Self::Segs(SegsView { op }),
785 OP_EDGES => Self::Edges(EdgesView { op }),
786 OP_TOUCH => Self::Touch(TouchView { op }),
787 OP_REMOVE => Self::Remove(key),
788 OP_PIN => Self::Pin(key, op[5] & 1 != 0),
789 OP_ATTRS => Self::Attrs(AttrsView { op }),
790 _ => return None,
791 })
792 }
793
794 #[must_use]
796 #[inline]
797 pub fn key(&self) -> UrlKey {
798 match self {
799 Self::Upsert(v) => v.key(),
800 Self::Segs(v) => v.key(),
801 Self::Edges(v) => v.key(),
802 Self::Touch(v) => v.key(),
803 Self::Attrs(v) => v.key(),
804 Self::Remove(k) | Self::Pin(k, _) => *k,
805 }
806 }
807}
808
809#[inline]
812fn text(op: &[u8], at: usize, len: usize) -> &str {
813 std::str::from_utf8(&op[at..at + len]).unwrap_or("")
814}
815
816#[inline]
817fn utf8(bytes: &[u8], what: &str) -> Result<()> {
818 std::str::from_utf8(bytes)
819 .map(|_| ())
820 .map_err(|_| Error::format(format!("{what}: invalid utf-8")))
821}
822
823pub fn validate_op(bytes: &[u8]) -> Result<OpView<'_>> {
828 let n = bytes.len();
829 if !(OP_HEADER_LEN..=MAX_OP).contains(&n) || get_u32(bytes, 0) as usize != n {
830 return Err(Error::format("op length disagrees with its header"));
831 }
832 let (kind, oflags) = (bytes[4], bytes[5]);
833 if get_u16(bytes, 6) != 0 {
834 return Err(Error::format("non-zero reserved op bytes"));
835 }
836 if oflags & !u8::from(kind == OP_PIN) != 0 {
837 return Err(Error::format("unknown op flags"));
838 }
839 match kind {
840 OP_UPSERT => check_upsert(bytes)?,
841 OP_SEGS => check_segs(bytes)?,
842 OP_EDGES => check_edges(bytes)?,
843 OP_TOUCH => check_touch(bytes)?,
844 OP_REMOVE | OP_PIN if n == OP_HEADER_LEN => {}
845 OP_REMOVE | OP_PIN => return Err(Error::format("remove/pin carries a body")),
846 OP_ATTRS => check_attrs(bytes)?,
847 other => return Err(Error::format(format!("unknown op kind {other}"))),
848 }
849 OpView::trusted(bytes).ok_or_else(|| Error::format("unknown op kind"))
850}
851
852#[inline]
853fn zeros(bytes: &[u8]) -> bool {
854 bytes.iter().all(|&b| b == 0)
855}
856
857#[inline]
858fn check_upsert(op: &[u8]) -> Result<()> {
859 let n = op.len();
860 if n < UPSERT_FIXED {
861 return Err(Error::format("upsert too short"));
862 }
863 let present = op[34];
864 if present & !(HAS_TITLE | HAS_SNIPPET | HAS_ETAG) != 0 || op[35] != 0 || !zeros(&op[44..48]) {
865 return Err(Error::format("upsert: unknown bits"));
866 }
867 let lens = [36, 38, 40, 42].map(|at| usize::from(get_u16(op, at)));
868 if lens[0] == 0 || lens.iter().any(|&l| l > MAX_LABEL) {
869 return Err(Error::format("upsert: string length out of range"));
870 }
871 for (len, bit) in lens[1..].iter().zip([HAS_TITLE, HAS_SNIPPET, HAS_ETAG]) {
872 if present & bit == 0 && *len != 0 {
873 return Err(Error::format("upsert: an absent string has a length"));
874 }
875 }
876 if lens.iter().sum::<usize>() != n - UPSERT_FIXED {
877 return Err(Error::format("upsert: string lengths disagree with the op"));
878 }
879 let mut at = UPSERT_FIXED;
880 for len in lens {
881 utf8(&op[at..at + len], "upsert")?;
882 at += len;
883 }
884 Ok(())
885}
886
887#[inline]
888fn check_segs(op: &[u8]) -> Result<()> {
889 let n = op.len();
890 if n < SEGS_FIXED || !zeros(&op[17..24]) || usize::from(op[16]) > MAX_SEGS {
891 return Err(Error::format("segments: bad header"));
892 }
893 let count = usize::from(op[16]);
894 let mut at = SEGS_FIXED + count * SEG_ENTRY;
895 if at > n {
896 return Err(Error::format("segments: entries overrun the op"));
897 }
898 for i in 0..count {
899 let e = SEGS_FIXED + i * SEG_ENTRY;
900 let len = usize::from(get_u16(op, e + 10));
901 if len > MAX_LABEL || get_u16(op, e + 14) != 0 || len > n - at {
902 return Err(Error::format("segments: bad entry"));
903 }
904 utf8(&op[at..at + len], "segment label")?;
905 at += len;
906 }
907 if at != n {
908 return Err(Error::format("segments: label lengths disagree with the op"));
909 }
910 Ok(())
911}
912
913#[inline]
914fn check_edges(op: &[u8]) -> Result<()> {
915 let n = op.len();
916 if n < EDGES_FIXED || !zeros(&op[18..24]) {
917 return Err(Error::format("edges: bad header"));
918 }
919 let count = usize::from(get_u16(op, 16));
920 if count > MAX_EDGES || n != EDGES_FIXED + count * EDGE_ENTRY {
921 return Err(Error::format("edges: count disagrees with the op"));
922 }
923 let mut prev: Option<u64> = None;
924 for i in 0..count {
925 let dst = get_u64(op, EDGES_FIXED + i * EDGE_ENTRY);
926 if prev.is_some_and(|p| p >= dst) {
927 return Err(Error::format("edges: destinations not strictly ascending"));
928 }
929 prev = Some(dst);
930 }
931 Ok(())
932}
933
934#[inline]
935fn check_touch(op: &[u8]) -> Result<()> {
936 let n = op.len();
937 if n < TOUCH_FIXED {
938 return Err(Error::format("touch too short"));
939 }
940 let bits = op[49];
941 let len = usize::from(get_u16(op, 50));
942 if bits & !(T_HASH | T_ETAG | T_TOMBSTONE) != 0
943 || !zeros(&op[52..56])
944 || (bits & T_HASH == 0 && get_u64(op, 24) != 0)
945 || (bits & T_ETAG == 0 && len != 0)
946 || len > MAX_LABEL
947 || n != TOUCH_FIXED + len
948 {
949 return Err(Error::format("touch: bad fields"));
950 }
951 utf8(&op[TOUCH_FIXED..], "touch etag")
952}
953
954#[inline]
955fn check_attrs(op: &[u8]) -> Result<()> {
956 let n = op.len();
957 if n < ATTRS_FIXED || !zeros(&op[17..24]) || usize::from(op[16]) > MAX_ATTRS {
958 return Err(Error::format("attributes: bad header"));
959 }
960 let count = usize::from(op[16]);
961 let mut at = ATTRS_FIXED + count * ATTR_ENTRY;
962 if at > n {
963 return Err(Error::format("attributes: entries overrun the op"));
964 }
965 let mut prev: Option<u8> = None;
966 for i in 0..count {
967 let e = ATTRS_FIXED + i * ATTR_ENTRY;
968 let (k, len) = (op[e], get_u32(op, e + 4) as usize);
969 if prev.is_some_and(|p| p >= k)
970 || !zeros(&op[e + 1..e + 4])
971 || len > MAX_ATTR_LEN
972 || len > n - at
973 {
974 return Err(Error::format("attributes: bad entry"));
975 }
976 prev = Some(k);
977 utf8(&op[at..at + len], "attribute")?;
978 at += len;
979 }
980 if at != n {
981 return Err(Error::format("attributes: value lengths disagree with the op"));
982 }
983 Ok(())
984}
985
986#[derive(Clone, Copy, Debug)]
988pub struct UpsertView<'a> {
989 op: &'a [u8],
990}
991
992impl<'a> UpsertView<'a> {
993 #[must_use]
995 pub fn key(&self) -> UrlKey {
996 UrlKey(get_u64(self.op, 8))
997 }
998 #[must_use]
1000 pub fn content_hash(&self) -> u64 {
1001 get_u64(self.op, 16)
1002 }
1003 #[must_use]
1005 pub fn fetched_at_ms(&self) -> u64 {
1006 get_u64(self.op, 24)
1007 }
1008 #[must_use]
1010 pub fn flags(&self) -> u16 {
1011 get_u16(self.op, 32)
1012 }
1013 fn len_at(&self, i: usize) -> usize {
1014 usize::from(get_u16(self.op, 36 + 2 * i))
1015 }
1016 #[inline]
1017 fn field(&self, i: usize, bit: u8) -> Option<&'a str> {
1018 if i > 0 && self.op[34] & bit == 0 {
1019 return None;
1020 }
1021 let at = UPSERT_FIXED + (0..i).map(|j| self.len_at(j)).sum::<usize>();
1022 Some(text(self.op, at, self.len_at(i)))
1023 }
1024 #[must_use]
1026 pub fn url(&self) -> &'a str {
1027 self.field(0, 0).unwrap_or("")
1028 }
1029 #[must_use]
1031 pub fn title(&self) -> Option<&'a str> {
1032 self.field(1, HAS_TITLE)
1033 }
1034 #[must_use]
1036 pub fn snippet(&self) -> Option<&'a str> {
1037 self.field(2, HAS_SNIPPET)
1038 }
1039 #[must_use]
1041 pub fn etag(&self) -> Option<&'a str> {
1042 self.field(3, HAS_ETAG)
1043 }
1044}
1045
1046#[derive(Clone, Copy, Debug)]
1048pub struct SegsView<'a> {
1049 op: &'a [u8],
1050}
1051
1052#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1054pub struct SegView<'a> {
1055 pub label: &'a str,
1057 pub byte_start: u32,
1059 pub byte_len: u32,
1061 pub importance: u16,
1063 pub depth: u8,
1065 pub flags: u8,
1067}
1068
1069impl<'a> SegsView<'a> {
1070 #[must_use]
1072 pub fn key(&self) -> UrlKey {
1073 UrlKey(get_u64(self.op, 8))
1074 }
1075 #[must_use]
1077 pub fn len(&self) -> usize {
1078 usize::from(self.op[16])
1079 }
1080 #[must_use]
1082 pub fn is_empty(&self) -> bool {
1083 self.len() == 0
1084 }
1085 pub fn iter(&self) -> impl Iterator<Item = SegView<'a>> + 'a {
1087 let (op, n) = (self.op, self.len());
1088 let mut label = SEGS_FIXED + n * SEG_ENTRY;
1089 (0..n).map(move |i| {
1090 let e = SEGS_FIXED + i * SEG_ENTRY;
1091 let len = usize::from(get_u16(op, e + 10));
1092 let s = SegView {
1093 label: text(op, label, len),
1094 byte_start: get_u32(op, e),
1095 byte_len: get_u32(op, e + 4),
1096 importance: get_u16(op, e + 8),
1097 depth: op[e + 12],
1098 flags: op[e + 13],
1099 };
1100 label += len;
1101 s
1102 })
1103 }
1104}
1105
1106#[derive(Clone, Copy, Debug)]
1108pub struct EdgesView<'a> {
1109 op: &'a [u8],
1110}
1111
1112impl<'a> EdgesView<'a> {
1113 #[must_use]
1115 pub fn key(&self) -> UrlKey {
1116 UrlKey(get_u64(self.op, 8))
1117 }
1118 #[must_use]
1120 pub fn len(&self) -> usize {
1121 usize::from(get_u16(self.op, 16))
1122 }
1123 #[must_use]
1125 pub fn is_empty(&self) -> bool {
1126 self.len() == 0
1127 }
1128 #[must_use]
1130 pub fn get(&self, i: usize) -> Option<Edge> {
1131 (i < self.len()).then(|| edge_at(self.entries(), i))
1132 }
1133 pub fn iter(&self) -> impl Iterator<Item = Edge> + 'a {
1135 let e = self.entries();
1136 (0..e.len() / EDGE_ENTRY).map(move |i| edge_at(e, i))
1137 }
1138 #[must_use]
1140 pub(crate) fn entries(&self) -> &'a [u8] {
1141 &self.op[EDGES_FIXED..]
1142 }
1143}
1144
1145#[inline]
1147pub(crate) fn edge_at(entries: &[u8], i: usize) -> Edge {
1148 let e = i * EDGE_ENTRY;
1149 (UrlKey(get_u64(entries, e)), entries[e + 8], entries[e + 9], get_u16(entries, e + 10))
1150}
1151
1152#[inline]
1154pub(crate) fn edge_count(entries: &[u8]) -> usize {
1155 entries.len() / EDGE_ENTRY
1156}
1157
1158#[derive(Clone, Copy, Debug)]
1160pub struct TouchView<'a> {
1161 op: &'a [u8],
1162}
1163
1164impl<'a> TouchView<'a> {
1165 #[must_use]
1167 pub fn key(&self) -> UrlKey {
1168 UrlKey(get_u64(self.op, 8))
1169 }
1170 #[must_use]
1172 pub fn checked_at_ms(&self) -> u64 {
1173 get_u64(self.op, 16)
1174 }
1175 #[must_use]
1177 pub fn content_hash(&self) -> Option<u64> {
1178 (self.op[49] & T_HASH != 0).then(|| get_u64(self.op, 24))
1179 }
1180 #[must_use]
1182 pub fn last_change_ms(&self) -> u64 {
1183 get_u64(self.op, 32)
1184 }
1185 #[must_use]
1187 pub fn interval_s(&self) -> u32 {
1188 get_u32(self.op, 40)
1189 }
1190 #[must_use]
1192 pub fn checks(&self) -> u16 {
1193 get_u16(self.op, 44)
1194 }
1195 #[must_use]
1197 pub fn changes(&self) -> u16 {
1198 get_u16(self.op, 46)
1199 }
1200 #[must_use]
1202 pub fn outcome(&self) -> u8 {
1203 self.op[48]
1204 }
1205 #[must_use]
1207 #[inline]
1208 pub fn etag(&self) -> Option<&'a str> {
1209 (self.op[49] & T_ETAG != 0)
1210 .then(|| text(self.op, TOUCH_FIXED, usize::from(get_u16(self.op, 50))))
1211 }
1212 #[must_use]
1214 pub fn tombstone(&self) -> bool {
1215 self.op[49] & T_TOMBSTONE != 0
1216 }
1217}
1218
1219#[derive(Clone, Copy, Debug)]
1221pub struct AttrsView<'a> {
1222 op: &'a [u8],
1223}
1224
1225impl<'a> AttrsView<'a> {
1226 #[must_use]
1228 pub fn key(&self) -> UrlKey {
1229 UrlKey(get_u64(self.op, 8))
1230 }
1231 #[must_use]
1233 pub fn len(&self) -> usize {
1234 usize::from(self.op[16])
1235 }
1236 #[must_use]
1238 pub fn is_empty(&self) -> bool {
1239 self.len() == 0
1240 }
1241 pub fn iter(&self) -> impl Iterator<Item = (u8, &'a str)> + 'a {
1243 let (op, n) = (self.op, self.len());
1244 let mut value = ATTRS_FIXED + n * ATTR_ENTRY;
1245 (0..n).map(move |i| {
1246 let e = ATTRS_FIXED + i * ATTR_ENTRY;
1247 let len = get_u32(op, e + 4) as usize;
1248 let v = (op[e], text(op, value, len));
1249 value += len;
1250 v
1251 })
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258
1259 use crate::writer::{DocRecord, SegmentRecord};
1262
1263 fn doc(url: &str) -> DocRecord<'_> {
1264 DocRecord {
1265 url,
1266 url_key: UrlKey::of(url),
1267 content_hash: 0xAB,
1268 fetched_at_ms: 77,
1269 title: Some("Title"),
1270 snippet: None,
1271 etag: Some("\"e1\""),
1272 pinned: false,
1273 }
1274 }
1275
1276 fn every_kind() -> RecordBuf {
1278 let mut b = RecordBuf::new();
1279 b.push_upsert(&doc("https://x.dev/a"), 1).unwrap();
1280 let segs = [
1281 SegmentRecord {
1282 label: "Install",
1283 byte_range: Some((10, 200)),
1284 depth: 1,
1285 importance: 60_000,
1286 },
1287 SegmentRecord {
1288 label: "Ünïcode › Linux", byte_range: None, depth: 0, importance: 7
1289 },
1290 ];
1291 b.push_segments(UrlKey(9), &segs).unwrap();
1292 b.push_edges(UrlKey(9), &[(UrlKey(11), 0, 0, 65_535), (UrlKey(400), 2, 3, 100)]).unwrap();
1293 let touch = TouchRecord {
1294 checked_at_ms: 5,
1295 outcome: 1,
1296 content_hash: Some(42),
1297 etag: Some("\"v2\""),
1298 interval_s: 3600,
1299 checks: 3,
1300 changes: 1,
1301 last_change_ms: 4,
1302 tombstone: true,
1303 };
1304 b.push_touch(UrlKey(9), &touch).unwrap();
1305 b.push_remove(UrlKey(11));
1306 b.push_pin(UrlKey(9), true);
1307 b.push_attrs(UrlKey(9), &[(1, "INT64"), (2, &"SELECT 1".repeat(1000))]).unwrap();
1308 b
1309 }
1310
1311 fn record(b: &RecordBuf) -> Record<'_> {
1312 match next_record(b.as_bytes(), 0).unwrap() {
1313 Next::Record(r) => r,
1314 Next::Torn => panic!("a sealed record reads as torn"),
1315 }
1316 }
1317
1318 #[test]
1319 fn a_record_round_trips_every_op_kind() {
1320 let mut buf = every_kind();
1321 buf.seal(41).unwrap();
1322 let rec = record(&buf);
1323 assert_eq!(
1324 (rec.first_seq(), rec.op_count(), rec.last_seq(), rec.len()),
1325 (41, 7, 47, buf.len())
1326 );
1327 let ops: Vec<(u64, usize, OpView<'_>)> = rec.ops().collect();
1328 assert_eq!(ops.iter().map(|o| o.0).collect::<Vec<_>>(), (41..=47).collect::<Vec<_>>());
1329 for &(_, off, _) in &ops {
1330 let len = get_u32(buf.as_bytes(), off) as usize;
1331 validate_op(&buf.as_bytes()[off..off + len]).expect("each op validates alone");
1332 }
1333 let OpView::Upsert(up) = ops[0].2 else { panic!("upsert") };
1334 assert_eq!(up.key(), UrlKey::of("https://x.dev/a"));
1335 assert_eq!((up.content_hash(), up.fetched_at_ms(), up.flags()), (0xAB, 77, 1));
1336 assert_eq!(
1337 (up.url(), up.title(), up.snippet(), up.etag()),
1338 ("https://x.dev/a", Some("Title"), None, Some("\"e1\""))
1339 );
1340 let OpView::Segs(sv) = ops[1].2 else { panic!("segments") };
1341 let segs: Vec<SegView<'_>> = sv.iter().collect();
1342 assert_eq!(segs.len(), 2);
1343 assert_eq!(
1344 (segs[0].label, segs[0].byte_start, segs[0].byte_len, segs[0].depth, segs[0].flags),
1345 ("Install", 10, 200, 1, 0)
1346 );
1347 assert_eq!((segs[1].label, segs[1].depth, segs[1].importance), ("Ünïcode › Linux", 1, 7));
1348 assert_eq!(segs[1].flags, crate::format::base::sflags::NO_RANGE);
1349 let OpView::Edges(ev) = ops[2].2 else { panic!("edges") };
1350 assert_eq!(
1351 ev.iter().collect::<Vec<_>>(),
1352 [(UrlKey(11), 0, 0, 65_535), (UrlKey(400), 2, 3, 100)]
1353 );
1354 assert_eq!(ev.get(1), Some((UrlKey(400), 2, 3, 100)));
1355 assert_eq!(ev.get(2), None);
1356 let OpView::Touch(tv) = ops[3].2 else { panic!("touch") };
1357 assert_eq!(
1358 (tv.checked_at_ms(), tv.outcome(), tv.content_hash(), tv.etag()),
1359 (5, 1, Some(42), Some("\"v2\""))
1360 );
1361 assert_eq!(
1362 (tv.interval_s(), tv.checks(), tv.changes(), tv.last_change_ms(), tv.tombstone()),
1363 (3600, 3, 1, 4, true)
1364 );
1365 assert!(matches!(ops[4].2, OpView::Remove(UrlKey(11))));
1366 assert!(matches!(ops[5].2, OpView::Pin(UrlKey(9), true)));
1367 let OpView::Attrs(av) = ops[6].2 else { panic!("attributes") };
1368 let attrs: Vec<(u8, &str)> = av.iter().collect();
1369 assert_eq!(attrs[0], (1, "INT64"));
1370 assert_eq!(attrs[1].1.len(), 8000);
1371 assert_eq!(buf.op_views().count(), 7);
1373 }
1374
1375 #[test]
1379 fn a_cut_at_every_byte_tears_only_the_record_it_lands_in() {
1380 let mut image = Vec::new();
1381 let mut ends = Vec::new();
1382 for (i, n) in [3u64, 1, 5].into_iter().enumerate() {
1383 let mut b = RecordBuf::new();
1384 for k in 0..n {
1385 b.push_remove(UrlKey(k));
1386 }
1387 b.push_upsert(&doc("https://x.dev/z"), 0).unwrap();
1388 b.seal(1 + 10 * i as u64).unwrap();
1389 image.extend_from_slice(b.as_bytes());
1390 ends.push(image.len());
1391 }
1392 for cut in 0..=image.len() {
1393 let bytes = &image[..cut];
1394 let mut pos = 0;
1395 while let Next::Record(r) = next_record(bytes, pos).unwrap() {
1396 pos += r.len();
1397 }
1398 let want = ends.iter().copied().filter(|&e| e <= cut).max().unwrap_or(0);
1399 assert_eq!(pos, want, "cut {cut}");
1400 }
1401 }
1402
1403 fn reseal(rec: &mut [u8]) {
1406 let sum = record_sum(rec);
1407 rec[24..32].copy_from_slice(&sum.to_le_bytes());
1408 }
1409
1410 fn corrupt_after(mutate: impl FnOnce(&mut Vec<u8>)) -> Error {
1411 let mut b = every_kind();
1412 b.seal(1).unwrap();
1413 let mut rec = b.into_vec();
1414 mutate(&mut rec);
1415 let n = rec.len() as u32;
1416 rec[0..4].copy_from_slice(&n.to_le_bytes());
1417 reseal(&mut rec);
1418 match next_record(&rec, 0) {
1419 Err(e @ Error::Corrupt { .. }) => e,
1420 Ok(Next::Torn) => panic!("a checksummed record read as torn"),
1421 Ok(Next::Record(_)) => panic!("accepted a malformed record"),
1422 Err(e) => panic!("not corrupt: {e}"),
1423 }
1424 }
1425
1426 fn op_offsets() -> Vec<usize> {
1428 let mut b = every_kind();
1429 b.seal(1).unwrap();
1430 let offs = record(&b).ops().map(|o| o.1).collect();
1431 offs
1432 }
1433
1434 type Tamper = fn(&mut Vec<u8>, &[usize]);
1436
1437 #[test]
1438 fn a_checksummed_but_malformed_record_is_corrupt_never_torn() {
1439 let cases: [(&str, Tamper); 16] = [
1440 ("unknown kind", |r, o| r[o[0] + 4] = 7),
1441 ("reserved op bytes", |r, o| r[o[2] + 6] = 1),
1442 ("reserved record byte", |r, _| r[7] = 1),
1443 ("reserved record word", |r, _| r[21] = 1),
1444 ("unknown op flags", |r, o| r[o[1] + 5] = 1),
1445 ("bad utf-8", |r, o| r[o[0] + 48] = 0xff),
1446 ("descending edges", |r, o| {
1447 r[o[2] + 24..o[2] + 32].copy_from_slice(&u64::MAX.to_le_bytes());
1448 }),
1449 ("descending attribute keys", |r, o| r[o[6] + 24] = 9),
1450 ("string lengths", |r, o| r[o[0] + 36] += 1),
1451 ("absent string with a length", |r, o| r[o[0] + 40] = 1),
1452 ("touch hash without its bit", |r, o| r[o[3] + 49] &= !1),
1453 ("op count too high", |r, _| r[16] = 8),
1454 ("op count too low", |r, _| r[16] = 6),
1455 ("first seq zero", |r, _| r[8..16].copy_from_slice(&0u64.to_le_bytes())),
1456 ("seq overflow", |r, _| r[8..16].copy_from_slice(&u64::MAX.to_le_bytes())),
1457 ("trailing bytes", |r, _| r.extend_from_slice(&[0; 3])),
1458 ];
1459 let offs = op_offsets();
1460 for (what, tamper) in cases {
1461 let e = corrupt_after(|r| tamper(r, &offs));
1462 assert!(e.to_string().contains("wal record at byte 0"), "{what}: {e}");
1463 }
1464 }
1465
1466 #[test]
1467 fn a_short_mistagged_or_mismatched_record_is_torn() {
1468 let mut b = every_kind();
1469 b.seal(1).unwrap();
1470 let good = b.into_vec();
1471 let torn = |mutate: fn(&mut Vec<u8>)| {
1472 let mut r = good.clone();
1473 mutate(&mut r);
1474 matches!(next_record(&r, 0), Ok(Next::Torn))
1475 };
1476 assert!(torn(|r| r[4] ^= 1), "tag");
1477 assert!(torn(|r| r[6] = 2), "flags");
1478 assert!(torn(|r| r[100] ^= 1), "checksum");
1479 assert!(
1480 torn(|r| r[0..4].copy_from_slice(&(MIN_RECORD as u32 - 1).to_le_bytes())),
1481 "too short"
1482 );
1483 assert!(torn(|r| r[0..4].copy_from_slice(&u32::MAX.to_le_bytes())), "too long");
1484 assert!(torn(|r| r.truncate(RECORD_HEADER_LEN - 1)), "no header");
1485 assert!(matches!(next_record(&good, good.len() + 5), Ok(Next::Torn)), "past the end");
1486 }
1487
1488 #[test]
1489 fn staging_refuses_bad_input_and_appends_nothing() {
1490 let mut b = RecordBuf::new();
1491 b.push_remove(UrlKey(1));
1492 let mark = b.mark();
1493 let long = "x".repeat(MAX_LABEL + 1);
1494 assert!(b.push_upsert(&doc(""), 0).is_err());
1495 assert!(b
1496 .push_upsert(&DocRecord { title: Some(&long), ..doc("https://x.dev/a") }, 0)
1497 .is_err());
1498 assert!(b.push_edges(UrlKey(1), &[(UrlKey(5), 0, 0, 0), (UrlKey(5), 0, 0, 0)]).is_err());
1499 assert!(b.push_edges(UrlKey(1), &vec![(UrlKey(5), 0, 0, 0); MAX_EDGES + 1]).is_err());
1500 assert!(b.push_attrs(UrlKey(1), &[(2, "a"), (1, "b")]).is_err());
1501 let touch = TouchRecord {
1502 checked_at_ms: 0,
1503 outcome: 0,
1504 content_hash: None,
1505 etag: Some(&long),
1506 interval_s: 0,
1507 checks: 0,
1508 changes: 0,
1509 last_change_ms: 0,
1510 tombstone: false,
1511 };
1512 assert!(b.push_touch(UrlKey(1), &touch).is_err());
1513 assert_eq!(b.mark(), mark, "a refused push appended something");
1514 b.push_pin(UrlKey(2), false);
1515 b.truncate(mark);
1516 assert_eq!(b.ops(), 1);
1517 b.truncate(RecordBuf::new().mark());
1518 assert!(b.is_empty() && b.as_bytes().is_empty());
1519 assert!(b.seal(1).is_err(), "an empty record");
1520 }
1521
1522 #[test]
1525 fn the_op_cap_admits_a_maximal_attribute_set_exactly() {
1526 assert_eq!(MAX_OP - ATTRS_FIXED - MAX_ATTRS * ATTR_ENTRY, 1_048_246);
1527 let big = "v".repeat(MAX_ATTR_LEN);
1528 let last = "w".repeat(1_048_246 - 3 * MAX_ATTR_LEN);
1529 let mut set: Vec<(u8, &str)> = (0..64u8).map(|k| (k, "")).collect();
1530 for (k, v) in [big.as_str(), &big, &big, &last].into_iter().enumerate() {
1531 set[k].1 = v;
1532 }
1533 let mut b = RecordBuf::new();
1534 b.push_attrs(UrlKey(1), &set).expect("exactly MAX_OP is accepted");
1535 assert_eq!(b.len(), RECORD_HEADER_LEN + MAX_OP);
1536 let one_more = format!("{last}w");
1537 set[3].1 = &one_more;
1538 assert!(RecordBuf::new().push_attrs(UrlKey(1), &set).is_err(), "MAX_OP + 1 is refused");
1539 b.seal(1).unwrap();
1540 assert!(matches!(next_record(b.as_bytes(), 0), Ok(Next::Record(_))));
1541 }
1542
1543 #[test]
1546 fn the_record_cap_admits_exactly_max_record() {
1547 let big = "v".repeat(MAX_ATTR_LEN);
1548 let fixed = ATTRS_FIXED + ATTR_ENTRY;
1549 let mut b = RecordBuf::new();
1550 let mut key = 0;
1551 while b.len() < MAX_RECORD {
1552 let room = MAX_RECORD - b.len().max(RECORD_HEADER_LEN);
1553 b.push_attrs(UrlKey(key), &[(0, &big[..(room - fixed).min(MAX_ATTR_LEN)])]).unwrap();
1554 key += 1;
1555 }
1556 assert_eq!(b.len(), MAX_RECORD);
1557 b.seal(1).expect("exactly MAX_RECORD is accepted");
1558 assert!(matches!(next_record(b.as_bytes(), 0), Ok(Next::Record(_))));
1559 let before = b.mark();
1560 b.push_pin(UrlKey(1), true);
1561 let e = b.seal(1).unwrap_err();
1562 assert!(matches!(e, Error::Format { .. }), "{e}");
1563 assert_eq!(b.ops(), before.ops + 1, "a refused seal keeps the ops");
1564 }
1565
1566 fn image(generation: u64) -> Vec<u8> {
1570 let mut image = encode_header(generation, 0);
1571 let mut all = every_kind();
1572 all.seal(1).unwrap();
1573 for (i, (_, off, _)) in record(&all).ops().enumerate() {
1574 let len = get_u32(all.as_bytes(), off) as usize;
1575 let mut b = RecordBuf::new();
1576 b.buf.extend_from_slice(&[0; RECORD_HEADER_LEN]);
1577 b.buf.extend_from_slice(&all.as_bytes()[off..off + len]);
1578 b.ops = 1;
1579 b.seal(i as u64 + 1).unwrap();
1580 image.extend_from_slice(b.as_bytes());
1581 }
1582 image
1583 }
1584
1585 fn seqs(r: &Replay<'_>) -> Vec<u64> {
1586 r.records().flat_map(|r| r.first_seq()..=r.last_seq()).collect()
1587 }
1588
1589 #[test]
1590 fn wal_replays_and_stops_at_tear() {
1591 let image = image(7);
1592 let full = replay(&image).unwrap();
1593 assert_eq!(seqs(&full), (1..=7).collect::<Vec<_>>());
1594 assert_eq!(full.good_len, image.len());
1595 assert_eq!(full.base_generation, 7);
1596 for cut in HEADER_LEN..image.len() {
1598 let r = replay(&image[..cut]).unwrap();
1599 assert!(seqs(&r).len() <= 7);
1600 assert!(r.good_len <= cut);
1601 }
1602 }
1603
1604 #[test]
1605 fn a_bad_checksum_ends_replay_and_a_bad_header_errors() {
1606 let mut image = encode_header(1, 0);
1607 let mut b = RecordBuf::new();
1608 b.push_remove(UrlKey(1));
1609 b.seal(1).unwrap();
1610 image.extend_from_slice(b.as_bytes());
1611 let good = image.len();
1612 b.seal(2).unwrap();
1613 image.extend_from_slice(b.as_bytes());
1614 image[good + 26] ^= 0xff; let r = replay(&image).unwrap();
1616 assert_eq!(seqs(&r), [1]);
1617 assert_eq!(r.good_len, good);
1618
1619 let mut bad_header = encode_header(1, 0);
1620 bad_header[3] ^= 0xff;
1621 assert!(replay(&bad_header).is_err());
1622 }
1623
1624 #[test]
1625 fn committed_len_matches_replay_at_every_cut() {
1626 let image = image(3);
1627 for cut in 0..=image.len() {
1628 let want = replay(&image[..cut]).map_or(0, |r| r.good_len);
1629 assert_eq!(committed_len(&image[..cut]), want, "cut {cut}");
1630 if cut >= HEADER_LEN {
1631 assert_eq!(committed_frames_len(&image[HEADER_LEN..cut]) + HEADER_LEN, want);
1632 }
1633 }
1634 }
1635
1636 #[test]
1637 fn streamed_record_checksum_equals_the_one_shot_hash() {
1638 let mut b = every_kind();
1639 b.seal(7).unwrap();
1640 let rec = b.as_bytes();
1641 let mut one_shot = rec[..24].to_vec();
1642 one_shot.extend_from_slice(&rec[RECORD_HEADER_LEN..]);
1643 assert_eq!(get_u64(rec, 24), xxh3_64(&one_shot));
1644 }
1645
1646 #[test]
1647 fn sequence_regression_is_corrupt() {
1648 let mut image = encode_header(1, 0);
1649 for first in [5, 4] {
1650 let mut b = RecordBuf::new();
1651 b.push_remove(UrlKey(first));
1652 b.seal(first).unwrap();
1653 image.extend_from_slice(b.as_bytes());
1654 }
1655 assert!(matches!(replay(&image), Err(Error::Corrupt { .. })));
1656 assert_eq!(committed_len(&image), image.len() - (image.len() - HEADER_LEN) / 2);
1658 let mut image = encode_header(1, 0);
1660 for (first, n) in [(1, 3), (3, 1)] {
1661 let mut b = RecordBuf::new();
1662 for k in 0..n {
1663 b.push_remove(UrlKey(k));
1664 }
1665 b.seal(first).unwrap();
1666 image.extend_from_slice(b.as_bytes());
1667 }
1668 assert!(matches!(replay(&image), Err(Error::Corrupt { .. })));
1669 }
1670
1671 #[test]
1674 fn a_version_2_log_is_a_typed_refusal() {
1675 let mut v2 = encode_header(1, 0);
1676 v2[4..6].copy_from_slice(&2u16.to_le_bytes());
1677 let sum = xxh3_64(&v2[..HEADER_LEN - 8]);
1678 v2[HEADER_LEN - 8..].copy_from_slice(&sum.to_le_bytes());
1679 v2.extend_from_slice(&[0x55; 40]); assert!(matches!(replay(&v2), Err(Error::Version { found: 2, supported: 3 })));
1681 assert!(matches!(parse_header(&v2), Err(Error::Version { found: 2, supported: 3 })));
1682 assert_eq!(committed_len(&v2), 0, "nothing of it is shipped as committed");
1683 }
1684}