1use std::convert::TryInto;
16
17pub const MAGIC: [u8; 4] = *b"RETE";
19
20pub const CURRENT_FORMAT_VERSION: u8 = 0x05;
34
35pub const MIN_STABLE_READ_VERSION: u8 = 0x05;
44
45pub const HEADER_LEN: usize = 1024;
47
48const SECTION_DIR_OFFSET: usize = 64;
50const SECTION_ENTRY_LEN: usize = 24;
52pub const MAX_SECTIONS: usize = (HEADER_LEN - SECTION_DIR_OFFSET) / SECTION_ENTRY_LEN;
54
55pub const FLAG_HAS_QUADS: u8 = 0b0000_0001;
57
58pub const FLAG_TILE_SYNOPSIS: u8 = 0b0000_0010;
63
64pub const FLAG_HAS_QUOTED_TRIPLES: u8 = 0b0000_0100;
71
72pub const FLAG_QUAD_INDEX: u8 = 0b0000_1000;
81
82pub const FLAG_TEXT_INDEX_NAMED_GRAPHS: u8 = 0b0001_0000;
89
90pub const TEXT_INDEX_PARTIAL_DIGEST_LEN: u64 = 16;
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum SectionKind {
99 Metadata,
101 Dictionary,
103 Index,
105 PyramidMeta,
107 NamedGraphs,
109 TextIndex,
111 TextIndexPartial,
125 BuildInfo,
132 LabelIndex,
138 Schema,
144 QuadIndex,
149 Unknown(u16),
152}
153
154impl SectionKind {
155 fn to_u16(self) -> u16 {
156 match self {
157 SectionKind::Metadata => 1,
158 SectionKind::Dictionary => 2,
159 SectionKind::Index => 3,
160 SectionKind::PyramidMeta => 4,
161 SectionKind::NamedGraphs => 5,
162 SectionKind::TextIndex => 6,
163 SectionKind::BuildInfo => 7,
164 SectionKind::LabelIndex => 8,
165 SectionKind::Schema => 9,
166 SectionKind::QuadIndex => 10,
167 SectionKind::TextIndexPartial => 11,
168 SectionKind::Unknown(k) => k,
169 }
170 }
171
172 fn from_u16(k: u16) -> Self {
173 match k {
174 1 => SectionKind::Metadata,
175 2 => SectionKind::Dictionary,
176 3 => SectionKind::Index,
177 4 => SectionKind::PyramidMeta,
178 5 => SectionKind::NamedGraphs,
179 6 => SectionKind::TextIndex,
180 7 => SectionKind::BuildInfo,
181 8 => SectionKind::LabelIndex,
182 9 => SectionKind::Schema,
183 10 => SectionKind::QuadIndex,
184 11 => SectionKind::TextIndexPartial,
185 other => SectionKind::Unknown(other),
186 }
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub struct Section {
194 pub kind: SectionKind,
195 pub flags: u16,
196 pub offset: u64,
197 pub length: u64,
198}
199
200#[derive(Debug, thiserror::Error)]
201#[non_exhaustive]
202pub enum HeaderError {
203 #[error("buffer too small: need {HEADER_LEN} bytes, got {0}")]
204 TooSmall(usize),
205 #[error("bad magic: expected RETE")]
206 BadMagic,
207 #[error(
208 "unsupported .rete format {found:#04x}; this Rete build reads {min:#04x}..={max:#04x}. Pre-1.0 files must be rebuilt from RDF source with `rete build`"
209 )]
210 UnsupportedVersion { found: u8, min: u8, max: u8 },
211 #[error("section count {0} overruns the header frame")]
212 BadSectionCount(usize),
213 #[error("unusable index permutation mask {mask:#04x}: {why}")]
214 BadPermMask { mask: u8, why: &'static str },
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct Header {
222 pub version: u8,
225 pub flags: u8,
226 pub metadata_offset: u64,
227 pub metadata_len: u64,
228 pub dictionary_offset: u64,
229 pub dictionary_len: u64,
230 pub root_dir_offset: u64,
231 pub root_dir_len: u64,
232 pub pyramid_meta_offset: u64,
233 pub pyramid_meta_len: u64,
234 pub dict_codec: u8,
235 pub block_codec: u8,
236 pub pyramid_levels: u16,
237 pub quad_count: u64,
238 pub term_count: u64,
239 pub content_hash: [u8; 16],
241 pub named_graphs_offset: u64,
243 pub named_graphs_len: u64,
244 pub schema_meta_len: u32,
249 pub text_index_offset: u64,
252 pub text_index_len: u64,
253 pub text_index_partial: bool,
257 pub build_info_offset: u64,
260 pub build_info_len: u64,
261 pub perms: crate::index::PermSet,
273 pub extra_sections: Vec<Section>,
278}
279
280impl Header {
281 pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
283 let mut b = [0u8; HEADER_LEN];
284 b[0..4].copy_from_slice(&MAGIC);
286 b[4] = self.version;
287 b[5] = self.flags;
288 b[6..8].copy_from_slice(&(HEADER_LEN as u16).to_le_bytes());
289 b[8..24].copy_from_slice(&self.content_hash);
290 b[24..32].copy_from_slice(&self.quad_count.to_le_bytes());
291 b[32..40].copy_from_slice(&self.term_count.to_le_bytes());
292 b[40..42].copy_from_slice(&self.pyramid_levels.to_le_bytes());
293 b[42] = self.dict_codec;
294 b[43] = self.block_codec;
295 b[46..50].copy_from_slice(&self.schema_meta_len.to_le_bytes());
297 b[50] = if self.perms == crate::index::PermSet::ALL {
300 0
301 } else {
302 self.perms.bits()
303 };
304 let entry = |kind, offset, length| Section {
311 kind,
312 flags: 0,
313 offset,
314 length,
315 };
316 let mut entries: Vec<Section> = vec![
317 entry(
318 SectionKind::Metadata,
319 self.metadata_offset,
320 self.metadata_len,
321 ),
322 entry(
323 SectionKind::Dictionary,
324 self.dictionary_offset,
325 self.dictionary_len,
326 ),
327 entry(SectionKind::Index, self.root_dir_offset, self.root_dir_len),
328 entry(
329 SectionKind::PyramidMeta,
330 self.pyramid_meta_offset,
331 self.pyramid_meta_len,
332 ),
333 entry(
334 SectionKind::NamedGraphs,
335 self.named_graphs_offset,
336 self.named_graphs_len,
337 ),
338 ];
339 if self.text_index_len > 0 {
340 entries.push(entry(
341 if self.text_index_partial {
342 SectionKind::TextIndexPartial
343 } else {
344 SectionKind::TextIndex
345 },
346 self.text_index_offset,
347 self.text_index_len,
348 ));
349 }
350 if self.build_info_len > 0 {
351 entries.push(entry(
352 SectionKind::BuildInfo,
353 self.build_info_offset,
354 self.build_info_len,
355 ));
356 }
357 entries.extend(self.extra_sections.iter().copied());
358 debug_assert!(
359 entries.len() <= MAX_SECTIONS,
360 "too many sections for a 1 KB header"
361 );
362 let n = entries.len().min(MAX_SECTIONS);
363 b[44..46].copy_from_slice(&(n as u16).to_le_bytes());
364 for (i, s) in entries.iter().take(n).enumerate() {
365 let p = SECTION_DIR_OFFSET + i * SECTION_ENTRY_LEN;
366 b[p..p + 2].copy_from_slice(&s.kind.to_u16().to_le_bytes());
367 b[p + 2..p + 4].copy_from_slice(&s.flags.to_le_bytes());
368 b[p + 8..p + 16].copy_from_slice(&s.offset.to_le_bytes());
370 b[p + 16..p + 24].copy_from_slice(&s.length.to_le_bytes());
371 }
372 b
373 }
374
375 pub fn from_bytes(b: &[u8]) -> Result<Self, HeaderError> {
377 if b.len() < HEADER_LEN {
378 return Err(HeaderError::TooSmall(b.len()));
379 }
380 if b[0..4] != MAGIC {
381 return Err(HeaderError::BadMagic);
382 }
383 if !(MIN_STABLE_READ_VERSION..=CURRENT_FORMAT_VERSION).contains(&b[4]) {
384 return Err(HeaderError::UnsupportedVersion {
385 found: b[4],
386 min: MIN_STABLE_READ_VERSION,
387 max: CURRENT_FORMAT_VERSION,
388 });
389 }
390 let u16_at = |o: usize| u16::from_le_bytes(b[o..o + 2].try_into().unwrap());
391 let u32_at = |o: usize| u32::from_le_bytes(b[o..o + 4].try_into().unwrap());
392 let u64_at = |o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());
393
394 let section_count = u16_at(44) as usize;
395 if SECTION_DIR_OFFSET + section_count * SECTION_ENTRY_LEN > HEADER_LEN {
396 return Err(HeaderError::BadSectionCount(section_count));
397 }
398
399 let perms = if b[50] == 0 {
400 crate::index::PermSet::ALL
401 } else {
402 crate::index::PermSet::from_bits(b[50])
403 .map_err(|why| HeaderError::BadPermMask { mask: b[50], why })?
404 };
405
406 let mut h = Header {
407 version: b[4],
408 flags: b[5],
409 metadata_offset: 0,
410 metadata_len: 0,
411 dictionary_offset: 0,
412 dictionary_len: 0,
413 root_dir_offset: 0,
414 root_dir_len: 0,
415 pyramid_meta_offset: 0,
416 pyramid_meta_len: 0,
417 dict_codec: b[42],
418 block_codec: b[43],
419 pyramid_levels: u16_at(40),
420 quad_count: u64_at(24),
421 term_count: u64_at(32),
422 content_hash: b[8..24].try_into().unwrap(),
423 named_graphs_offset: 0,
424 named_graphs_len: 0,
425 schema_meta_len: u32_at(46),
426 perms,
427 text_index_offset: 0,
428 text_index_len: 0,
429 text_index_partial: false,
430 build_info_offset: 0,
431 build_info_len: 0,
432 extra_sections: Vec::new(),
433 };
434 for i in 0..section_count {
435 let p = SECTION_DIR_OFFSET + i * SECTION_ENTRY_LEN;
436 let kind = SectionKind::from_u16(u16_at(p));
437 let offset = u64_at(p + 8);
438 let length = u64_at(p + 16);
439 match kind {
440 SectionKind::Metadata => {
441 h.metadata_offset = offset;
442 h.metadata_len = length;
443 }
444 SectionKind::Dictionary => {
445 h.dictionary_offset = offset;
446 h.dictionary_len = length;
447 }
448 SectionKind::Index => {
449 h.root_dir_offset = offset;
450 h.root_dir_len = length;
451 }
452 SectionKind::PyramidMeta => {
453 h.pyramid_meta_offset = offset;
454 h.pyramid_meta_len = length;
455 }
456 SectionKind::NamedGraphs => {
457 h.named_graphs_offset = offset;
458 h.named_graphs_len = length;
459 }
460 SectionKind::TextIndex => {
461 h.text_index_offset = offset;
462 h.text_index_len = length;
463 h.text_index_partial = false;
464 }
465 SectionKind::TextIndexPartial => {
466 h.text_index_offset = offset;
467 h.text_index_len = length;
468 h.text_index_partial = true;
469 }
470 SectionKind::BuildInfo => {
471 h.build_info_offset = offset;
472 h.build_info_len = length;
473 }
474 SectionKind::LabelIndex
475 | SectionKind::Schema
476 | SectionKind::QuadIndex
477 | SectionKind::Unknown(_) => h.extra_sections.push(Section {
478 kind,
479 flags: u16_at(p + 2),
480 offset,
481 length,
482 }),
483 }
484 }
485 Ok(h)
486 }
487
488 pub fn label_index_section(&self) -> Option<Section> {
490 self.section(SectionKind::LabelIndex)
491 .filter(|s| s.length > 0)
492 }
493
494 pub fn schema_section(&self) -> Option<Section> {
496 self.section(SectionKind::Schema).filter(|s| s.length > 0)
497 }
498
499 pub fn has_quads(&self) -> bool {
500 self.flags & FLAG_HAS_QUADS != 0
501 }
502
503 pub fn expected_file_len(&self) -> Option<u64> {
520 let mut end = HEADER_LEN as u64;
521 let named = [
522 (self.metadata_offset, self.metadata_len),
523 (self.dictionary_offset, self.dictionary_len),
524 (self.root_dir_offset, self.root_dir_len),
525 (self.pyramid_meta_offset, self.pyramid_meta_len),
526 (self.named_graphs_offset, self.named_graphs_len),
527 (self.text_index_offset, self.text_index_len),
528 (self.build_info_offset, self.build_info_len),
529 ];
530 let extras = self.extra_sections.iter().map(|s| (s.offset, s.length));
531 for (offset, length) in named.into_iter().chain(extras) {
532 if length == 0 {
533 continue; }
535 end = end.max(offset.checked_add(length)?);
536 }
537 end.checked_add(MAGIC.len() as u64)
538 }
539
540 pub fn has_quoted_triples(&self) -> bool {
542 self.flags & FLAG_HAS_QUOTED_TRIPLES != 0
543 }
544
545 pub fn has_tile_synopsis(&self) -> bool {
547 self.flags & FLAG_TILE_SYNOPSIS != 0
548 }
549
550 pub fn text_index_covers_named_graphs(&self) -> bool {
553 self.flags & FLAG_TEXT_INDEX_NAMED_GRAPHS != 0
554 }
555
556 pub fn text_index_payload(&self) -> (u64, u64) {
561 if self.text_index_partial {
562 (
563 self.text_index_offset
564 .saturating_add(TEXT_INDEX_PARTIAL_DIGEST_LEN),
565 self.text_index_len
566 .saturating_sub(TEXT_INDEX_PARTIAL_DIGEST_LEN),
567 )
568 } else {
569 (self.text_index_offset, self.text_index_len)
570 }
571 }
572
573 pub fn section(&self, kind: SectionKind) -> Option<Section> {
577 let (offset, length) = match kind {
578 SectionKind::Metadata => (self.metadata_offset, self.metadata_len),
579 SectionKind::Dictionary => (self.dictionary_offset, self.dictionary_len),
580 SectionKind::Index => (self.root_dir_offset, self.root_dir_len),
581 SectionKind::PyramidMeta => (self.pyramid_meta_offset, self.pyramid_meta_len),
582 SectionKind::NamedGraphs => (self.named_graphs_offset, self.named_graphs_len),
583 SectionKind::TextIndex if !self.text_index_partial => {
584 (self.text_index_offset, self.text_index_len)
585 }
586 SectionKind::TextIndexPartial if self.text_index_partial => {
587 (self.text_index_offset, self.text_index_len)
588 }
589 SectionKind::TextIndex | SectionKind::TextIndexPartial => (0, 0),
590 SectionKind::BuildInfo => (self.build_info_offset, self.build_info_len),
591 SectionKind::LabelIndex
592 | SectionKind::Schema
593 | SectionKind::QuadIndex
594 | SectionKind::Unknown(_) => {
595 return self.extra_sections.iter().find(|s| s.kind == kind).copied()
596 }
597 };
598 Some(Section {
599 kind,
600 flags: 0,
601 offset,
602 length,
603 })
604 }
605
606 pub fn with_section(mut self, kind: SectionKind, offset: u64, length: u64) -> Self {
610 match kind {
611 SectionKind::Metadata => {
612 self.metadata_offset = offset;
613 self.metadata_len = length;
614 }
615 SectionKind::Dictionary => {
616 self.dictionary_offset = offset;
617 self.dictionary_len = length;
618 }
619 SectionKind::Index => {
620 self.root_dir_offset = offset;
621 self.root_dir_len = length;
622 }
623 SectionKind::PyramidMeta => {
624 self.pyramid_meta_offset = offset;
625 self.pyramid_meta_len = length;
626 }
627 SectionKind::NamedGraphs => {
628 self.named_graphs_offset = offset;
629 self.named_graphs_len = length;
630 }
631 SectionKind::TextIndex => {
632 self.text_index_offset = offset;
633 self.text_index_len = length;
634 self.text_index_partial = false;
635 }
636 SectionKind::TextIndexPartial => {
637 self.text_index_offset = offset;
638 self.text_index_len = length;
639 self.text_index_partial = true;
640 }
641 SectionKind::BuildInfo => {
642 self.build_info_offset = offset;
643 self.build_info_len = length;
644 }
645 SectionKind::LabelIndex | SectionKind::Schema | SectionKind::QuadIndex => {
646 match self.extra_sections.iter_mut().find(|s| s.kind == kind) {
648 Some(s) => {
649 s.offset = offset;
650 s.length = length;
651 }
652 None => self.extra_sections.push(Section {
653 kind,
654 flags: 0,
655 offset,
656 length,
657 }),
658 }
659 }
660 SectionKind::Unknown(_) => self.extra_sections.push(Section {
661 kind,
662 flags: 0,
663 offset,
664 length,
665 }),
666 }
667 self
668 }
669
670 pub fn has_quad_index(&self) -> bool {
672 self.flags & FLAG_QUAD_INDEX != 0
673 }
674
675 pub fn quad_index_section(&self) -> Option<Section> {
677 self.section(SectionKind::QuadIndex)
678 .filter(|s| s.length > 0)
679 }
680
681 pub fn index_container_sections(&self) -> usize {
684 self.perms.len() + usize::from(self.has_quad_index())
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 fn sample() -> Header {
693 Header {
694 version: CURRENT_FORMAT_VERSION,
695 flags: FLAG_HAS_QUADS,
696 metadata_offset: 1024,
697 metadata_len: 42,
698 dictionary_offset: 1066,
699 dictionary_len: 2048,
700 root_dir_offset: 3114,
701 root_dir_len: 256,
702 pyramid_meta_offset: 3370,
703 pyramid_meta_len: 64,
704 dict_codec: 1,
705 block_codec: 2,
706 pyramid_levels: 3,
707 perms: crate::index::PermSet::ALL,
708 quad_count: 5,
709 term_count: 9,
710 content_hash: [7u8; 16],
711 named_graphs_offset: 3434,
712 named_graphs_len: 48,
713 schema_meta_len: 99,
714 text_index_offset: 0,
715 text_index_len: 0,
716 text_index_partial: false,
717 build_info_offset: 0,
718 build_info_len: 0,
719 extra_sections: Vec::new(),
720 }
721 }
722
723 #[test]
724 fn round_trip() {
725 let h = sample();
726 let bytes = h.to_bytes();
727 assert_eq!(bytes.len(), HEADER_LEN);
728 assert_eq!(&bytes[0..4], b"RETE");
729 let back = Header::from_bytes(&bytes).unwrap();
730 assert_eq!(h, back);
731 assert!(back.has_quads());
732 }
733
734 #[test]
735 fn byte_layout_matches_spec() {
736 let h = Header {
738 content_hash: [0xCC; 16],
739 quad_count: 0x99,
740 term_count: 0xAA,
741 pyramid_levels: 0xABCD,
742 dict_codec: 0xA1,
743 block_codec: 0xA2,
744 schema_meta_len: 0xD00D,
745 metadata_offset: 0x11,
746 metadata_len: 0x22,
747 dictionary_offset: 0x33,
748 dictionary_len: 0x44,
749 ..sample()
750 };
751 let b = h.to_bytes();
752 let u16_at = |o: usize| u16::from_le_bytes(b[o..o + 2].try_into().unwrap());
753 let u64_at = |o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());
754
755 assert_eq!(&b[0..4], b"RETE");
757 assert_eq!(b[4], CURRENT_FORMAT_VERSION);
758 assert_eq!(b[5], FLAG_HAS_QUADS);
759 assert_eq!(u16_at(6), HEADER_LEN as u16);
760 assert_eq!(&b[8..24], &[0xCC; 16]); assert_eq!(u64_at(24), 0x99); assert_eq!(u64_at(32), 0xAA); assert_eq!(u16_at(40), 0xABCD); assert_eq!(b[42], 0xA1); assert_eq!(b[43], 0xA2); assert_eq!(u16_at(44), 5); assert_eq!(u32::from_le_bytes(b[46..50].try_into().unwrap()), 0xD00D); assert_eq!(u16_at(64), 1); assert_eq!(u64_at(72), 0x11); assert_eq!(u64_at(80), 0x22); assert_eq!(u16_at(88), 2);
774 assert_eq!(u64_at(96), 0x33);
775 assert_eq!(u64_at(104), 0x44);
776 assert_eq!(b.len(), HEADER_LEN);
777 }
778
779 #[test]
780 fn expected_file_len_is_last_section_end_plus_footer() {
781 assert_eq!(sample().expected_file_len(), Some(3482 + 4));
783
784 let mut h = sample();
787 h.text_index_offset = 1 << 40;
788 h.text_index_len = 0;
789 assert_eq!(h.expected_file_len(), Some(3482 + 4));
790
791 let mut e = sample();
793 e.metadata_len = 0;
794 e.dictionary_len = 0;
795 e.root_dir_len = 0;
796 e.pyramid_meta_len = 0;
797 e.named_graphs_len = 0;
798 assert_eq!(e.expected_file_len(), Some(1024 + 4));
799
800 let mut c = sample();
803 c.named_graphs_offset = u64::MAX - 8;
804 c.named_graphs_len = 64;
805 assert_eq!(c.expected_file_len(), None);
806
807 let f = sample().with_section(SectionKind::Unknown(99), 1 << 20, 512);
809 assert_eq!(f.expected_file_len(), Some((1 << 20) + 512 + 4));
810 }
811
812 #[test]
813 fn rejects_bad_magic() {
814 let mut bytes = [0u8; HEADER_LEN];
815 bytes[4] = CURRENT_FORMAT_VERSION;
816 assert!(matches!(
817 Header::from_bytes(&bytes),
818 Err(HeaderError::BadMagic)
819 ));
820 }
821
822 #[test]
823 fn stable_reader_accepts_v1_baseline_and_rejects_pre_v1() {
824 let current = sample().to_bytes();
825 assert_eq!(current[4], 0x05);
826 assert_eq!(Header::from_bytes(¤t).unwrap().version, 0x05);
827
828 for old in 0x01..=0x04 {
829 let mut bytes = current;
830 bytes[4] = old;
831 let error = Header::from_bytes(&bytes).unwrap_err();
832 assert!(matches!(
833 &error,
834 HeaderError::UnsupportedVersion {
835 found,
836 min: 0x05,
837 max: 0x05
838 } if *found == old
839 ));
840 assert!(error
841 .to_string()
842 .contains("Pre-1.0 files must be rebuilt from RDF source with `rete build`"));
843 }
844
845 for unsupported in [0x00, 0x06, 0xff] {
846 let mut bytes = current;
847 bytes[4] = unsupported;
848 assert!(matches!(
849 Header::from_bytes(&bytes),
850 Err(HeaderError::UnsupportedVersion {
851 found,
852 min: 0x05,
853 max: 0x05
854 }) if found == unsupported
855 ));
856 }
857 }
858
859 #[test]
860 fn rejects_overrunning_section_count() {
861 let mut bad = sample().to_bytes();
862 bad[44..46].copy_from_slice(&9999u16.to_le_bytes());
863 assert!(matches!(
864 Header::from_bytes(&bad),
865 Err(HeaderError::BadSectionCount(9999))
866 ));
867 }
868
869 #[test]
870 fn unknown_section_survives_round_trip() {
871 let h = sample().with_section(SectionKind::Unknown(99), 4096, 512);
874 let back = Header::from_bytes(&h.to_bytes()).unwrap();
875 assert_eq!(back.extra_sections.len(), 1);
876 let s = back.section(SectionKind::Unknown(99)).unwrap();
877 assert_eq!((s.offset, s.length), (4096, 512));
878 let dict = back.section(SectionKind::Dictionary).unwrap();
880 assert_eq!(dict.offset, h.dictionary_offset);
881 assert_eq!(h, back);
882 }
883
884 #[test]
885 fn build_info_section_round_trips_and_is_optional() {
886 let plain = sample().to_bytes();
889 assert_eq!(u16::from_le_bytes(plain[44..46].try_into().unwrap()), 5);
890 assert_eq!(sample().section(SectionKind::BuildInfo).unwrap().length, 0);
891
892 let h = sample().with_section(SectionKind::BuildInfo, 1066, 300);
894 let bytes = h.to_bytes();
895 assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 6);
896 let back = Header::from_bytes(&bytes).unwrap();
897 assert_eq!(h, back);
898 let s = back.section(SectionKind::BuildInfo).unwrap();
899 assert_eq!((s.offset, s.length), (1066, 300));
900 assert!(back.extra_sections.is_empty(), "BuildInfo is a known kind");
901 let far = sample().with_section(SectionKind::BuildInfo, 1 << 21, 128);
903 assert_eq!(far.expected_file_len(), Some((1 << 21) + 128 + 4));
904 }
905
906 #[test]
907 fn label_index_and_schema_sections_are_known_extras() {
908 assert!(sample().label_index_section().is_none());
911 assert!(sample().schema_section().is_none());
912
913 let h = sample()
914 .with_section(SectionKind::LabelIndex, 8000, 300)
915 .with_section(SectionKind::Schema, 8300, 40);
916 let bytes = h.to_bytes();
917 assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 7);
918 assert_eq!(
920 u16::from_le_bytes(bytes[64 + 5 * 24..][..2].try_into().unwrap()),
921 8
922 );
923 assert_eq!(
924 u16::from_le_bytes(bytes[64 + 6 * 24..][..2].try_into().unwrap()),
925 9
926 );
927 let back = Header::from_bytes(&bytes).unwrap();
928 assert_eq!(h, back);
929 assert_eq!(back.extra_sections.len(), 2, "stored with the extras");
930 let l = back.label_index_section().unwrap();
931 assert_eq!((l.offset, l.length), (8000, 300));
932 let s = back.schema_section().unwrap();
933 assert_eq!((s.offset, s.length), (8300, 40));
934 assert_eq!(back.expected_file_len(), Some(8340 + 4));
935
936 let again = back.with_section(SectionKind::Schema, 9000, 10);
938 assert_eq!(again.extra_sections.len(), 2);
939 assert_eq!(again.schema_section().unwrap().offset, 9000);
940
941 let empty = sample().with_section(SectionKind::LabelIndex, 0, 0);
943 assert!(empty.label_index_section().is_none());
944 }
945
946 #[test]
947 fn quad_index_flag_and_section_round_trip() {
948 let plain = sample();
950 assert!(!plain.has_quad_index());
951 assert!(plain.quad_index_section().is_none());
952 assert_eq!(plain.index_container_sections(), 6);
953
954 let mut h = sample().with_section(SectionKind::QuadIndex, 7000, 900);
957 h.flags |= FLAG_QUAD_INDEX;
958 h.named_graphs_offset = 0;
959 h.named_graphs_len = 0;
960 let bytes = h.to_bytes();
961 assert_eq!(bytes[5] & 0b1000, 0b1000, "flag bit 3");
962 let n = u16::from_le_bytes(bytes[44..46].try_into().unwrap()) as usize;
963 let kinds: Vec<u16> = (0..n)
964 .map(|i| {
965 let p = SECTION_DIR_OFFSET + i * SECTION_ENTRY_LEN;
966 u16::from_le_bytes(bytes[p..p + 2].try_into().unwrap())
967 })
968 .collect();
969 assert!(kinds.contains(&10), "a kind-10 directory entry: {kinds:?}");
970 let back = Header::from_bytes(&bytes).unwrap();
971 assert_eq!(back, h);
972 assert!(back.has_quad_index());
973 let s = back.quad_index_section().unwrap();
974 assert_eq!(
975 (s.kind, s.offset, s.length),
976 (SectionKind::QuadIndex, 7000, 900)
977 );
978 assert_eq!(back.index_container_sections(), 7);
979 assert_eq!(back.expected_file_len(), Some(7000 + 900 + 4));
980 let mut lean = back.clone();
982 lean.perms = crate::index::PermSet::CORE;
983 assert_eq!(lean.index_container_sections(), 4);
984 let again = back.with_section(SectionKind::QuadIndex, 8000, 10);
986 assert_eq!(again.extra_sections.len(), 1);
987 assert_eq!(again.quad_index_section().unwrap().offset, 8000);
988 }
989
990 #[test]
991 fn text_index_section_round_trips_and_is_optional() {
992 assert_eq!(
995 u16::from_le_bytes(sample().to_bytes()[44..46].try_into().unwrap()),
996 5
997 );
998 assert!(sample().section(SectionKind::TextIndex).unwrap().length == 0);
999
1000 let h = sample().with_section(SectionKind::TextIndex, 5000, 4096);
1002 let bytes = h.to_bytes();
1003 assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 6);
1004 let back = Header::from_bytes(&bytes).unwrap();
1005 assert_eq!(h, back);
1006 let s = back.section(SectionKind::TextIndex).unwrap();
1007 assert_eq!((s.offset, s.length), (5000, 4096));
1008 assert!(back.extra_sections.is_empty(), "TextIndex is a known kind");
1009 assert!(!back.text_index_partial);
1010 }
1011
1012 #[test]
1016 fn partial_text_index_is_kind_11_and_invisible_as_kind_6() {
1017 let h = sample().with_section(SectionKind::TextIndexPartial, 5000, 4096);
1018 assert!(h.text_index_partial);
1019 let bytes = h.to_bytes();
1020 assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 6);
1021 let p = SECTION_DIR_OFFSET + 5 * SECTION_ENTRY_LEN;
1023 assert_eq!(u16::from_le_bytes(bytes[p..p + 2].try_into().unwrap()), 11);
1024 let back = Header::from_bytes(&bytes).unwrap();
1025 assert_eq!(h, back);
1026 assert!(back.text_index_partial);
1027 assert_eq!((back.text_index_offset, back.text_index_len), (5000, 4096));
1028 let s = back.section(SectionKind::TextIndexPartial).unwrap();
1029 assert_eq!((s.offset, s.length), (5000, 4096));
1030 assert_eq!(back.section(SectionKind::TextIndex).unwrap().length, 0);
1031 assert!(back.extra_sections.is_empty(), "kind 11 is known here");
1032
1033 let mut old_view = bytes;
1037 old_view[p..p + 2].copy_from_slice(&0x7777u16.to_le_bytes());
1038 let old = Header::from_bytes(&old_view).unwrap();
1039 assert_eq!(old.text_index_len, 0);
1040 assert_eq!(old.extra_sections.len(), 1);
1041 }
1042}