1use crate::bytesio::{self, get_u16, get_u32, get_u64, put_u16, put_u32, put_u64, Reader};
17use crate::container;
18use crate::error::{Error, Result};
19use crate::key::UrlKey;
20
21pub const MAGIC: u32 = 0x5250_5247;
23pub const VERSION: u16 = 3;
29
30pub const NODE_LEN: usize = 72;
32pub const SEG_LEN: usize = 24;
34pub const EDGE_LEN: usize = 8;
36pub const LEX_LEN: usize = 24;
38pub const RANK_LEN: usize = 2;
40pub const ATTR_LEN: usize = 12;
42pub const TRI_LEN: usize = 12;
45
46pub const NO_LABEL: u32 = u32::MAX;
48
49pub mod nflags {
51 pub const PINNED: u16 = 1;
53 pub const STUB: u16 = 2;
55 pub const TOMBSTONE: u16 = 4;
58}
59
60pub mod eflags {
62 pub const INFERRED: u8 = 2;
64 pub const EXACT: u8 = 4;
66}
67
68pub mod sflags {
70 pub const NO_RANGE: u8 = 1;
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76#[repr(u16)]
77pub enum SectionKind {
78 Nodes = 1,
80 Segs = 2,
82 EdgeIndex = 3,
84 Edges = 4,
86 Labels = 5,
88 Lexicon = 6,
90 Postings = 7,
92 Ranks = 8,
94 InEdgeIndex = 9,
96 InEdges = 10,
99 AttrIndex = 11,
101 Attrs = 12,
103 Blobs = 13,
105 LexOrder = 14,
108 Trigrams = 15,
111 TriPostings = 16,
113}
114
115pub const FORMAT: container::Format = container::Format { magic: MAGIC, version: VERSION };
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub struct Header {
121 pub flags: u16,
123 pub node_count: u32,
125 pub seg_count: u32,
127 pub edge_count: u32,
129 pub token_count: u32,
131 pub generation: u64,
133 pub wal_applied_seq: u64,
135 pub created_at_ms: u64,
137}
138
139impl Header {
140 fn owner(&self) -> Vec<u8> {
141 let mut o = Vec::with_capacity(40);
142 put_u32(&mut o, self.node_count);
143 put_u32(&mut o, self.seg_count);
144 put_u32(&mut o, self.edge_count);
145 put_u32(&mut o, self.token_count);
146 put_u64(&mut o, self.generation);
147 put_u64(&mut o, self.wal_applied_seq);
148 put_u64(&mut o, self.created_at_ms);
149 o
150 }
151
152 fn from_owner(flags: u16, o: &[u8]) -> Self {
153 Self {
154 flags,
155 node_count: get_u32(o, 0),
156 seg_count: get_u32(o, 4),
157 edge_count: get_u32(o, 8),
158 token_count: get_u32(o, 12),
159 generation: get_u64(o, 16),
160 wal_applied_seq: get_u64(o, 24),
161 created_at_ms: get_u64(o, 32),
162 }
163 }
164
165 pub(crate) fn parse(bytes: &[u8]) -> Result<Self> {
168 let (flags, owner) = container::peek(bytes, FORMAT)?;
169 Ok(Self::from_owner(flags, &owner))
170 }
171}
172
173pub fn generation_of(bytes: &[u8]) -> Result<u64> {
177 Header::parse(bytes).map(|h| h.generation)
178}
179
180#[derive(Debug)]
182pub struct BaseWriter {
183 inner: container::Writer,
184}
185
186impl BaseWriter {
187 #[must_use]
189 pub fn new(header: Header) -> Self {
190 let mut inner = container::Writer::new(FORMAT);
191 inner.flags(header.flags).owner(&header.owner());
192 Self { inner }
193 }
194
195 pub fn add_section(&mut self, kind: SectionKind, bytes: Vec<u8>) {
197 self.inner.section(kind as u16, bytes);
198 }
199
200 #[must_use]
202 pub fn finish(self) -> Vec<u8> {
203 self.inner.finish()
204 }
205}
206
207#[derive(Clone, Debug)]
211pub struct BaseDir {
212 pub header: Header,
214 dir: container::Dir,
215}
216
217impl BaseDir {
218 pub fn parse(bytes: &[u8]) -> Result<Self> {
223 let dir = container::Dir::parse(bytes, FORMAT)?;
224 let header = Header::from_owner(dir.flags, dir.owner());
225 let dir = Self { header, dir };
226 dir.check_counts(bytes)?;
227 Ok(dir)
228 }
229
230 #[allow(clippy::too_many_lines)] fn check_counts(&self, bytes: &[u8]) -> Result<()> {
234 let n = self.header.node_count as usize;
235 let expect = |kind, want: usize, name: &str| -> Result<()> {
236 if self.section(bytes, kind).len() == want {
237 Ok(())
238 } else {
239 Err(Error::format(format!("{name} section length mismatch")))
240 }
241 };
242 expect(SectionKind::Nodes, n * NODE_LEN, "nodes")?;
243 expect(SectionKind::Segs, self.header.seg_count as usize * SEG_LEN, "segs")?;
244 expect(SectionKind::Edges, self.header.edge_count as usize * EDGE_LEN, "edges")?;
245 expect(SectionKind::Lexicon, self.header.token_count as usize * LEX_LEN, "lexicon")?;
246 if n > 0 {
247 expect(SectionKind::EdgeIndex, (n + 1) * 4, "edge index")?;
248 expect(SectionKind::Ranks, n * RANK_LEN, "ranks")?;
249 }
250 let labels = self.section(bytes, SectionKind::Labels);
253 let check_label = |off: u32| -> Result<()> {
254 if off == NO_LABEL {
255 return Ok(());
256 }
257 let off = off as usize;
258 if off + 2 > labels.len() {
259 return Err(Error::format("label offset out of bounds"));
260 }
261 let len = usize::from(get_u16(labels, off));
262 if off + 2 + len > labels.len() {
263 return Err(Error::format("label extends past heap"));
264 }
265 std::str::from_utf8(&labels[off + 2..off + 2 + len])
266 .map_err(|_| Error::format("label not utf-8"))?;
267 Ok(())
268 };
269 let nodes = self.section(bytes, SectionKind::Nodes);
270 for i in 0..n {
271 let r = NodeRec(&nodes[i * NODE_LEN..(i + 1) * NODE_LEN]);
272 for off in [r.url_off(), r.title_off(), r.snippet_off(), r.etag_off()] {
273 check_label(off)?;
274 }
275 let (s, c) = (r.seg_start() as usize, usize::from(r.seg_count()));
276 if s + c > self.header.seg_count as usize {
277 return Err(Error::format("segment run out of bounds"));
278 }
279 }
280 let segs = self.section(bytes, SectionKind::Segs);
281 for i in 0..self.header.seg_count as usize {
282 check_label(SegRec(&segs[i * SEG_LEN..(i + 1) * SEG_LEN]).label_off())?;
283 }
284 if n > 0 {
286 let idx = self.section(bytes, SectionKind::EdgeIndex);
287 let mut prev = 0u32;
288 for i in 0..=n {
289 let v = get_u32(idx, i * 4);
290 if v < prev || v > self.header.edge_count {
291 return Err(Error::format("edge index not monotone"));
292 }
293 prev = v;
294 }
295 if prev != self.header.edge_count {
296 return Err(Error::format("edge index does not cover edges"));
297 }
298 let edges = self.section(bytes, SectionKind::Edges);
299 for i in 0..self.header.edge_count as usize {
300 if get_u32(edges, i * EDGE_LEN) >= self.header.node_count {
301 return Err(Error::format("edge destination out of range"));
302 }
303 }
304 }
305 if n > 0 {
308 let check_csr = |kind, total: usize, name: &str| -> Result<()> {
309 let idx = self.section(bytes, kind);
310 if idx.is_empty() && total == 0 {
311 return Ok(());
312 }
313 if idx.len() != (n + 1) * 4 {
314 return Err(Error::format(format!("{name} index length mismatch")));
315 }
316 let mut prev = 0u32;
317 for i in 0..=n {
318 let v = get_u32(idx, i * 4);
319 if v < prev || v as usize > total {
320 return Err(Error::format(format!("{name} index not monotone")));
321 }
322 prev = v;
323 }
324 if prev as usize == total {
325 Ok(())
326 } else {
327 Err(Error::format(format!("{name} index does not cover its records")))
328 }
329 };
330 let in_edges = self.section(bytes, SectionKind::InEdges);
331 if in_edges.len() != self.header.edge_count as usize * EDGE_LEN {
332 return Err(Error::format("inbound edges length mismatch"));
333 }
334 check_csr(SectionKind::InEdgeIndex, self.header.edge_count as usize, "inbound edge")?;
335 for i in 0..self.header.edge_count as usize {
336 if get_u32(in_edges, i * EDGE_LEN) >= self.header.node_count {
337 return Err(Error::format("inbound edge source out of range"));
338 }
339 }
340 let attrs = self.section(bytes, SectionKind::Attrs);
341 if !attrs.len().is_multiple_of(ATTR_LEN) {
342 return Err(Error::format("attribute section length mismatch"));
343 }
344 check_csr(SectionKind::AttrIndex, attrs.len() / ATTR_LEN, "attribute")?;
345 let blobs = self.section(bytes, SectionKind::Blobs);
346 for rec in attrs.chunks_exact(ATTR_LEN) {
347 let (o, l) = (get_u32(rec, 4) as usize, get_u32(rec, 8) as usize);
348 let v = o.checked_add(l).and_then(|e| blobs.get(o..e));
349 if v.is_none_or(|v| std::str::from_utf8(v).is_err()) || rec[1..4] != [0; 3] {
350 return Err(Error::format("attribute value out of bounds or not utf-8"));
351 }
352 }
353 }
354 let vocab = self.header.token_count;
356 let order = self.section(bytes, SectionKind::LexOrder);
357 if order.len() != vocab as usize * 4
358 || order.chunks_exact(4).any(|c| get_u32(c, 0) >= vocab)
359 {
360 return Err(Error::format("lexicon order out of range"));
361 }
362 let (tri, tri_post) = (
363 self.section(bytes, SectionKind::Trigrams),
364 self.section(bytes, SectionKind::TriPostings),
365 );
366 if !tri.len().is_multiple_of(TRI_LEN) || !tri_post.len().is_multiple_of(4) {
367 return Err(Error::format("trigram section length mismatch"));
368 }
369 for rec in tri.chunks_exact(TRI_LEN) {
370 let (o, l) = (get_u32(rec, 4) as usize, get_u32(rec, 8) as usize);
371 let ids = o.checked_add(l).and_then(|e| tri_post.get(o * 4..e * 4));
372 if ids.is_none_or(|ids| ids.chunks_exact(4).any(|c| get_u32(c, 0) >= vocab)) {
373 return Err(Error::format("trigram postings out of range"));
374 }
375 }
376 let postings = self.section(bytes, SectionKind::Postings);
378 let lex = self.section(bytes, SectionKind::Lexicon);
379 let mut prev_hash = None;
380 for i in 0..self.header.token_count as usize {
381 let r = LexRec(&lex[i * LEX_LEN..(i + 1) * LEX_LEN]);
382 if let Some(p) = prev_hash {
383 if r.token_hash() <= p {
384 return Err(Error::format("lexicon not sorted"));
385 }
386 }
387 prev_hash = Some(r.token_hash());
388 let (o, l) = (r.postings_off() as usize, r.postings_len() as usize);
389 if o.checked_add(l).is_none_or(|e| e > postings.len()) {
390 return Err(Error::format("postings range out of bounds"));
391 }
392 check_label(r.label_off())?;
393 }
394 let mut prev_key = None;
396 for i in 0..n {
397 let k = NodeRec(&nodes[i * NODE_LEN..(i + 1) * NODE_LEN]).url_key();
398 if let Some(p) = prev_key {
399 if k <= p {
400 return Err(Error::format("nodes not sorted by url_key"));
401 }
402 }
403 prev_key = Some(k);
404 }
405 Ok(())
406 }
407
408 #[must_use]
410 pub fn section<'a>(&self, bytes: &'a [u8], kind: SectionKind) -> &'a [u8] {
411 self.dir.section(bytes, kind as u16)
412 }
413}
414
415pub fn validate(bytes: &[u8]) -> Result<()> {
417 BaseDir::parse(bytes).map(|_| ())
418}
419
420#[derive(Clone, Copy, Debug)]
422pub struct NodeRec<'a>(pub &'a [u8]);
423
424#[allow(missing_docs)]
425impl NodeRec<'_> {
426 #[must_use]
427 pub fn url_key(self) -> UrlKey {
428 UrlKey(get_u64(self.0, 0))
429 }
430 #[must_use]
431 pub fn content_hash(self) -> u64 {
432 get_u64(self.0, 8)
433 }
434 #[must_use]
435 pub fn fetched_at_ms(self) -> u64 {
436 get_u64(self.0, 16)
437 }
438 #[must_use]
439 pub fn last_change_ms(self) -> u64 {
440 get_u64(self.0, 24)
441 }
442 #[must_use]
443 pub fn url_off(self) -> u32 {
444 get_u32(self.0, 32)
445 }
446 #[must_use]
447 pub fn title_off(self) -> u32 {
448 get_u32(self.0, 36)
449 }
450 #[must_use]
451 pub fn snippet_off(self) -> u32 {
452 get_u32(self.0, 40)
453 }
454 #[must_use]
455 pub fn etag_off(self) -> u32 {
456 get_u32(self.0, 44)
457 }
458 #[must_use]
459 pub fn seg_start(self) -> u32 {
460 get_u32(self.0, 48)
461 }
462 #[must_use]
463 pub fn seg_count(self) -> u16 {
464 get_u16(self.0, 52)
465 }
466 #[must_use]
467 pub fn flags(self) -> u16 {
468 get_u16(self.0, 54)
469 }
470 #[must_use]
471 pub fn interval_s(self) -> u32 {
472 get_u32(self.0, 56)
473 }
474 #[must_use]
475 pub fn checks(self) -> u16 {
476 get_u16(self.0, 60)
477 }
478 #[must_use]
479 pub fn changes(self) -> u16 {
480 get_u16(self.0, 62)
481 }
482 #[must_use]
483 pub fn in_degree(self) -> u32 {
484 get_u32(self.0, 64)
485 }
486}
487
488#[allow(clippy::too_many_arguments)]
490pub(crate) fn write_node_rec(
491 out: &mut Vec<u8>,
492 url_key: UrlKey,
493 content_hash: u64,
494 fetched_at_ms: u64,
495 last_change_ms: u64,
496 label_offs: [u32; 4],
497 seg_start: u32,
498 seg_count: u16,
499 flags: u16,
500 interval_s: u32,
501 checks: u16,
502 changes: u16,
503 in_degree: u32,
504) {
505 put_u64(out, url_key.0);
506 put_u64(out, content_hash);
507 put_u64(out, fetched_at_ms);
508 put_u64(out, last_change_ms);
509 for off in label_offs {
510 put_u32(out, off);
511 }
512 put_u32(out, seg_start);
513 put_u16(out, seg_count);
514 put_u16(out, flags);
515 put_u32(out, interval_s);
516 put_u16(out, checks);
517 put_u16(out, changes);
518 put_u32(out, in_degree);
519 put_u32(out, 0); }
521
522#[derive(Clone, Copy, Debug)]
524pub struct SegRec<'a>(pub &'a [u8]);
525
526#[allow(missing_docs)]
527impl SegRec<'_> {
528 #[must_use]
529 pub fn seg_key(self) -> u64 {
530 get_u64(self.0, 0)
531 }
532 #[must_use]
533 pub fn byte_start(self) -> u32 {
534 get_u32(self.0, 8)
535 }
536 #[must_use]
537 pub fn byte_len(self) -> u32 {
538 get_u32(self.0, 12)
539 }
540 #[must_use]
541 pub fn label_off(self) -> u32 {
542 get_u32(self.0, 16)
543 }
544 #[must_use]
545 pub fn importance(self) -> u16 {
546 get_u16(self.0, 20)
547 }
548 #[must_use]
549 pub fn depth(self) -> u8 {
550 self.0[22]
551 }
552 #[must_use]
553 pub fn flags(self) -> u8 {
554 self.0[23]
555 }
556}
557
558#[allow(clippy::too_many_arguments)]
559pub(crate) fn write_seg_rec(
560 out: &mut Vec<u8>,
561 seg_key: u64,
562 byte_start: u32,
563 byte_len: u32,
564 label_off: u32,
565 importance: u16,
566 depth: u8,
567 flags: u8,
568) {
569 put_u64(out, seg_key);
570 put_u32(out, byte_start);
571 put_u32(out, byte_len);
572 put_u32(out, label_off);
573 put_u16(out, importance);
574 out.push(depth);
575 out.push(flags);
576}
577
578#[derive(Clone, Copy, Debug)]
580pub struct LexRec<'a>(pub &'a [u8]);
581
582#[allow(missing_docs)]
583impl LexRec<'_> {
584 #[must_use]
585 pub fn token_hash(self) -> u64 {
586 get_u64(self.0, 0)
587 }
588 #[must_use]
589 pub fn df(self) -> u32 {
590 get_u32(self.0, 8)
591 }
592 #[must_use]
593 pub fn postings_off(self) -> u32 {
594 get_u32(self.0, 12)
595 }
596 #[must_use]
597 pub fn postings_len(self) -> u32 {
598 get_u32(self.0, 16)
599 }
600 #[must_use]
601 pub fn label_off(self) -> u32 {
602 get_u32(self.0, 20)
603 }
604}
605
606pub(crate) fn write_lex_rec(
607 out: &mut Vec<u8>,
608 token_hash: u64,
609 df: u32,
610 postings_off: u32,
611 postings_len: u32,
612 label_off: u32,
613) {
614 put_u64(out, token_hash);
615 put_u32(out, df);
616 put_u32(out, postings_off);
617 put_u32(out, postings_len);
618 put_u32(out, label_off);
619}
620
621#[must_use]
624pub fn label_at(labels: &[u8], off: u32) -> Option<&str> {
625 if off == NO_LABEL {
626 return None;
627 }
628 let off = off as usize;
629 let len = usize::from(get_u16(labels, off));
630 std::str::from_utf8(&labels[off + 2..off + 2 + len]).ok()
631}
632
633pub(crate) fn push_label(labels: &mut Vec<u8>, s: &str) -> u32 {
635 let off = labels.len() as u32;
636 bytesio::put_str(labels, s);
637 off
638}
639
640#[must_use]
642pub fn find_node(nodes: &[u8], key: UrlKey) -> Option<u32> {
643 let n = nodes.len() / NODE_LEN;
644 let (mut lo, mut hi) = (0usize, n);
645 while lo < hi {
646 let mid = usize::midpoint(lo, hi);
647 let k = get_u64(nodes, mid * NODE_LEN);
648 match k.cmp(&key.0) {
649 std::cmp::Ordering::Less => lo = mid + 1,
650 std::cmp::Ordering::Greater => hi = mid,
651 std::cmp::Ordering::Equal => return Some(mid as u32),
652 }
653 }
654 None
655}
656
657pub fn decode_postings(bytes: &[u8]) -> Result<Vec<u32>> {
659 let mut r = Reader::new(bytes);
660 let mut out = Vec::with_capacity(bytes.len().min(r.remaining()));
661 let mut prev: u64 = 0;
662 while r.remaining() > 0 {
663 let delta = r.varint()?;
664 let id = if out.is_empty() {
665 delta
666 } else {
667 prev.checked_add(1)
668 .and_then(|v| v.checked_add(delta))
669 .ok_or_else(|| Error::format("posting delta overflow"))?
670 };
671 if id > u64::from(u32::MAX) {
672 return Err(Error::format("posting id overflow"));
673 }
674 out.push(id as u32);
675 prev = id;
676 }
677 Ok(out)
678}
679
680pub(crate) fn encode_postings(ids: &[u32], out: &mut Vec<u8>) {
682 let mut prev: Option<u32> = None;
683 for &id in ids {
684 let delta = match prev {
685 None => u64::from(id),
686 Some(p) => u64::from(id - p - 1),
687 };
688 bytesio::put_varint(out, delta);
689 prev = Some(id);
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696
697 fn empty_header() -> Header {
698 Header {
699 flags: 0,
700 node_count: 0,
701 seg_count: 0,
702 edge_count: 0,
703 token_count: 0,
704 generation: 1,
705 wal_applied_seq: 0,
706 created_at_ms: 0,
707 }
708 }
709
710 #[test]
711 fn empty_file_round_trips() {
712 let bytes = BaseWriter::new(empty_header()).finish();
713 let dir = BaseDir::parse(&bytes).unwrap();
714 assert_eq!(dir.header, empty_header());
715 }
716
717 #[test]
718 fn flipped_byte_anywhere_is_rejected() {
719 let bytes = BaseWriter::new(empty_header()).finish();
720 for i in 0..bytes.len() {
721 let mut bad = bytes.clone();
722 bad[i] ^= 0x01;
723 assert!(BaseDir::parse(&bad).is_err(), "flip at {i} accepted");
724 }
725 }
726
727 #[test]
728 fn truncation_at_every_landmark_is_rejected() {
729 let bytes = BaseWriter::new(empty_header()).finish();
730 for cut in 0..bytes.len() {
731 assert!(BaseDir::parse(&bytes[..cut]).is_err());
732 }
733 }
734
735 #[test]
736 fn postings_round_trip_and_reject_overflow() {
737 let ids = vec![0, 1, 5, 6, 1000, 70_000];
738 let mut enc = Vec::new();
739 encode_postings(&ids, &mut enc);
740 assert_eq!(decode_postings(&enc).unwrap(), ids);
741 assert!(decode_postings(&[0xff; 12]).is_err());
742 let mut hostile = Vec::new();
745 crate::bytesio::put_varint(&mut hostile, 1);
746 crate::bytesio::put_varint(&mut hostile, u64::MAX);
747 assert!(decode_postings(&hostile).is_err());
748 }
749
750 #[test]
751 fn node_binary_search_finds_all() {
752 let mut nodes = Vec::new();
753 for k in [3u64, 9, 12, 400, 500] {
754 write_node_rec(&mut nodes, UrlKey(k), 0, 0, 0, [NO_LABEL; 4], 0, 0, 0, 0, 0, 0, 0);
755 }
756 for k in [3u64, 9, 12, 400, 500] {
757 assert!(find_node(&nodes, UrlKey(k)).is_some());
758 }
759 assert!(find_node(&nodes, UrlKey(10)).is_none());
760 assert!(find_node(&nodes, UrlKey(0)).is_none());
761 assert!(find_node(&nodes, UrlKey(501)).is_none());
762 }
763}