1use std::convert::TryInto;
16
17pub const MAGIC: [u8; 4] = *b"RETE";
19
20pub const CURRENT_FORMAT_VERSION: u8 = 0x06;
34
35pub const LEGACY_FORMAT_VERSION: u8 = 0x05;
40
41pub const MIN_STABLE_READ_VERSION: u8 = 0x05;
50
51pub const HEADER_LEN: usize = 1024;
53
54const SECTION_DIR_OFFSET: usize = 64;
56const SECTION_ENTRY_LEN: usize = 24;
58pub const MAX_SECTIONS: usize = (HEADER_LEN - SECTION_DIR_OFFSET) / SECTION_ENTRY_LEN;
60
61pub const FLAG_HAS_QUADS: u8 = 0b0000_0001;
63
64pub const FLAG_TILE_SYNOPSIS: u8 = 0b0000_0010;
69
70pub const FLAG_HAS_QUOTED_TRIPLES: u8 = 0b0000_0100;
77
78pub const FLAG_QUAD_INDEX: u8 = 0b0000_1000;
87
88pub const FLAG_TEXT_INDEX_NAMED_GRAPHS: u8 = 0b0001_0000;
95
96pub const TEXT_INDEX_PARTIAL_DIGEST_LEN: u64 = 16;
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum SectionKind {
105 Metadata,
107 Dictionary,
109 Index,
111 PyramidMeta,
113 NamedGraphs,
115 TextIndex,
117 TextIndexPartial,
131 BuildInfo,
138 LabelIndex,
144 Schema,
150 QuadIndex,
155 Unknown(u16),
158}
159
160impl SectionKind {
161 fn to_u16(self) -> u16 {
162 match self {
163 SectionKind::Metadata => 1,
164 SectionKind::Dictionary => 2,
165 SectionKind::Index => 3,
166 SectionKind::PyramidMeta => 4,
167 SectionKind::NamedGraphs => 5,
168 SectionKind::TextIndex => 6,
169 SectionKind::BuildInfo => 7,
170 SectionKind::LabelIndex => 8,
171 SectionKind::Schema => 9,
172 SectionKind::QuadIndex => 10,
173 SectionKind::TextIndexPartial => 11,
174 SectionKind::Unknown(k) => k,
175 }
176 }
177
178 fn from_u16(k: u16) -> Self {
179 match k {
180 1 => SectionKind::Metadata,
181 2 => SectionKind::Dictionary,
182 3 => SectionKind::Index,
183 4 => SectionKind::PyramidMeta,
184 5 => SectionKind::NamedGraphs,
185 6 => SectionKind::TextIndex,
186 7 => SectionKind::BuildInfo,
187 8 => SectionKind::LabelIndex,
188 9 => SectionKind::Schema,
189 10 => SectionKind::QuadIndex,
190 11 => SectionKind::TextIndexPartial,
191 other => SectionKind::Unknown(other),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub struct Section {
200 pub kind: SectionKind,
201 pub flags: u16,
202 pub offset: u64,
203 pub length: u64,
204}
205
206#[derive(Debug, thiserror::Error)]
207#[non_exhaustive]
208pub enum HeaderError {
209 #[error("buffer too small: need {HEADER_LEN} bytes, got {0}")]
210 TooSmall(usize),
211 #[error("bad magic: expected RETE")]
212 BadMagic,
213 #[error(
214 "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`"
215 )]
216 UnsupportedVersion { found: u8, min: u8, max: u8 },
217 #[error("section count {0} overruns the header frame")]
218 BadSectionCount(usize),
219 #[error("unusable index permutation mask {mask:#04x}: {why}")]
220 BadPermMask { mask: u8, why: &'static str },
221 #[error("malformed 0x06 header: {0}")]
222 Malformed06(&'static str),
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct Header {
230 pub version: u8,
233 pub flags: u8,
234 pub metadata_offset: u64,
235 pub metadata_len: u64,
236 pub dictionary_offset: u64,
237 pub dictionary_len: u64,
238 pub root_dir_offset: u64,
239 pub root_dir_len: u64,
240 pub pyramid_meta_offset: u64,
241 pub pyramid_meta_len: u64,
242 pub dict_codec: u8,
243 pub block_codec: u8,
244 pub pyramid_levels: u16,
245 pub id_widths: u8,
247 pub quad_count: u64,
248 pub term_count: u64,
249 pub content_hash: [u8; 16],
251 pub named_graphs_offset: u64,
253 pub named_graphs_len: u64,
254 pub schema_meta_len: u32,
259 pub text_index_offset: u64,
262 pub text_index_len: u64,
263 pub text_index_partial: bool,
267 pub build_info_offset: u64,
270 pub build_info_len: u64,
271 pub perms: crate::index::PermSet,
283 pub extra_sections: Vec<Section>,
288}
289
290impl Header {
291 pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
293 let mut b = [0u8; HEADER_LEN];
294 b[0..4].copy_from_slice(&MAGIC);
296 b[4] = self.version;
297 b[5] = self.flags;
298 b[6..8].copy_from_slice(&(HEADER_LEN as u16).to_le_bytes());
299 b[8..24].copy_from_slice(&self.content_hash);
300 b[24..32].copy_from_slice(&self.quad_count.to_le_bytes());
301 b[32..40].copy_from_slice(&self.term_count.to_le_bytes());
302 b[40..42].copy_from_slice(&self.pyramid_levels.to_le_bytes());
303 b[42] = self.dict_codec;
304 b[43] = self.block_codec;
305 b[46..50].copy_from_slice(&self.schema_meta_len.to_le_bytes());
307 b[50] = if self.perms == crate::index::PermSet::ALL {
310 0
311 } else {
312 self.perms.bits()
313 };
314 let entry = |kind, offset, length| Section {
321 kind,
322 flags: 0,
323 offset,
324 length,
325 };
326 let mut entries: Vec<Section> = vec![
327 entry(
328 SectionKind::Metadata,
329 self.metadata_offset,
330 self.metadata_len,
331 ),
332 entry(
333 SectionKind::Dictionary,
334 self.dictionary_offset,
335 self.dictionary_len,
336 ),
337 entry(SectionKind::Index, self.root_dir_offset, self.root_dir_len),
338 entry(
339 SectionKind::PyramidMeta,
340 self.pyramid_meta_offset,
341 self.pyramid_meta_len,
342 ),
343 entry(
344 SectionKind::NamedGraphs,
345 self.named_graphs_offset,
346 self.named_graphs_len,
347 ),
348 ];
349 if self.text_index_len > 0 {
350 entries.push(entry(
351 if self.text_index_partial {
352 SectionKind::TextIndexPartial
353 } else {
354 SectionKind::TextIndex
355 },
356 self.text_index_offset,
357 self.text_index_len,
358 ));
359 }
360 if self.build_info_len > 0 {
361 entries.push(entry(
362 SectionKind::BuildInfo,
363 self.build_info_offset,
364 self.build_info_len,
365 ));
366 }
367 entries.extend(self.extra_sections.iter().copied());
368 debug_assert!(
369 entries.len() <= MAX_SECTIONS,
370 "too many sections for a 1 KB header"
371 );
372 let n = entries.len().min(MAX_SECTIONS);
373 b[44..46].copy_from_slice(&(n as u16).to_le_bytes());
374 for (i, s) in entries.iter().take(n).enumerate() {
375 let p = SECTION_DIR_OFFSET + i * SECTION_ENTRY_LEN;
376 b[p..p + 2].copy_from_slice(&s.kind.to_u16().to_le_bytes());
377 b[p + 2..p + 4].copy_from_slice(&s.flags.to_le_bytes());
378 b[p + 8..p + 16].copy_from_slice(&s.offset.to_le_bytes());
380 b[p + 16..p + 24].copy_from_slice(&s.length.to_le_bytes());
381 }
382 b
383 }
384
385 pub fn from_bytes(b: &[u8]) -> Result<Self, HeaderError> {
387 if b.len() < HEADER_LEN {
388 return Err(HeaderError::TooSmall(b.len()));
389 }
390 if b[0..4] != MAGIC {
391 return Err(HeaderError::BadMagic);
392 }
393 if b[4] == crate::v06::VERSION {
394 return crate::v06::Header06::from_bytes(b)
397 .map(|h| crate::v06::logical_header(&h))
398 .map_err(|e| match e {
399 crate::file::FileError::Container(m) => HeaderError::Malformed06(m),
400 _ => HeaderError::Malformed06("unreadable"),
401 });
402 }
403 if !(MIN_STABLE_READ_VERSION..=CURRENT_FORMAT_VERSION).contains(&b[4]) {
404 return Err(HeaderError::UnsupportedVersion {
405 found: b[4],
406 min: MIN_STABLE_READ_VERSION,
407 max: CURRENT_FORMAT_VERSION,
408 });
409 }
410 let u16_at = |o: usize| u16::from_le_bytes(b[o..o + 2].try_into().unwrap());
411 let u32_at = |o: usize| u32::from_le_bytes(b[o..o + 4].try_into().unwrap());
412 let u64_at = |o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());
413
414 let section_count = u16_at(44) as usize;
415 if SECTION_DIR_OFFSET + section_count * SECTION_ENTRY_LEN > HEADER_LEN {
416 return Err(HeaderError::BadSectionCount(section_count));
417 }
418
419 let perms = if b[50] == 0 {
420 crate::index::PermSet::ALL
421 } else {
422 crate::index::PermSet::from_bits(b[50])
423 .map_err(|why| HeaderError::BadPermMask { mask: b[50], why })?
424 };
425
426 let mut h = Header {
427 version: b[4],
428 flags: b[5],
429 metadata_offset: 0,
430 metadata_len: 0,
431 dictionary_offset: 0,
432 dictionary_len: 0,
433 root_dir_offset: 0,
434 root_dir_len: 0,
435 pyramid_meta_offset: 0,
436 pyramid_meta_len: 0,
437 dict_codec: b[42],
438 block_codec: b[43],
439 pyramid_levels: u16_at(40),
440 id_widths: 0,
441 quad_count: u64_at(24),
442 term_count: u64_at(32),
443 content_hash: b[8..24].try_into().unwrap(),
444 named_graphs_offset: 0,
445 named_graphs_len: 0,
446 schema_meta_len: u32_at(46),
447 perms,
448 text_index_offset: 0,
449 text_index_len: 0,
450 text_index_partial: false,
451 build_info_offset: 0,
452 build_info_len: 0,
453 extra_sections: Vec::new(),
454 };
455 for i in 0..section_count {
456 let p = SECTION_DIR_OFFSET + i * SECTION_ENTRY_LEN;
457 let kind = SectionKind::from_u16(u16_at(p));
458 let offset = u64_at(p + 8);
459 let length = u64_at(p + 16);
460 match kind {
461 SectionKind::Metadata => {
462 h.metadata_offset = offset;
463 h.metadata_len = length;
464 }
465 SectionKind::Dictionary => {
466 h.dictionary_offset = offset;
467 h.dictionary_len = length;
468 }
469 SectionKind::Index => {
470 h.root_dir_offset = offset;
471 h.root_dir_len = length;
472 }
473 SectionKind::PyramidMeta => {
474 h.pyramid_meta_offset = offset;
475 h.pyramid_meta_len = length;
476 }
477 SectionKind::NamedGraphs => {
478 h.named_graphs_offset = offset;
479 h.named_graphs_len = length;
480 }
481 SectionKind::TextIndex => {
482 h.text_index_offset = offset;
483 h.text_index_len = length;
484 h.text_index_partial = false;
485 }
486 SectionKind::TextIndexPartial => {
487 h.text_index_offset = offset;
488 h.text_index_len = length;
489 h.text_index_partial = true;
490 }
491 SectionKind::BuildInfo => {
492 h.build_info_offset = offset;
493 h.build_info_len = length;
494 }
495 SectionKind::LabelIndex
496 | SectionKind::Schema
497 | SectionKind::QuadIndex
498 | SectionKind::Unknown(_) => h.extra_sections.push(Section {
499 kind,
500 flags: u16_at(p + 2),
501 offset,
502 length,
503 }),
504 }
505 }
506 Ok(h)
507 }
508
509 pub fn label_index_section(&self) -> Option<Section> {
511 self.section(SectionKind::LabelIndex)
512 .filter(|s| s.length > 0)
513 }
514
515 pub fn schema_section(&self) -> Option<Section> {
517 self.section(SectionKind::Schema).filter(|s| s.length > 0)
518 }
519
520 pub fn has_quads(&self) -> bool {
521 self.flags & FLAG_HAS_QUADS != 0
522 }
523
524 pub fn expected_file_len(&self) -> Option<u64> {
541 if self.version == crate::v06::VERSION {
542 return Some(self.metadata_offset);
545 }
546 let mut end = HEADER_LEN as u64;
547 let named = [
548 (self.metadata_offset, self.metadata_len),
549 (self.dictionary_offset, self.dictionary_len),
550 (self.root_dir_offset, self.root_dir_len),
551 (self.pyramid_meta_offset, self.pyramid_meta_len),
552 (self.named_graphs_offset, self.named_graphs_len),
553 (self.text_index_offset, self.text_index_len),
554 (self.build_info_offset, self.build_info_len),
555 ];
556 let extras = self.extra_sections.iter().map(|s| (s.offset, s.length));
557 for (offset, length) in named.into_iter().chain(extras) {
558 if length == 0 {
559 continue; }
561 end = end.max(offset.checked_add(length)?);
562 }
563 end.checked_add(MAGIC.len() as u64)
564 }
565
566 pub fn has_quoted_triples(&self) -> bool {
568 self.flags & FLAG_HAS_QUOTED_TRIPLES != 0
569 }
570
571 pub fn has_tile_synopsis(&self) -> bool {
573 self.flags & FLAG_TILE_SYNOPSIS != 0
574 }
575
576 pub fn text_index_covers_named_graphs(&self) -> bool {
579 self.flags & FLAG_TEXT_INDEX_NAMED_GRAPHS != 0
580 }
581
582 pub fn text_index_payload(&self) -> (u64, u64) {
587 if self.text_index_partial {
588 (
589 self.text_index_offset
590 .saturating_add(TEXT_INDEX_PARTIAL_DIGEST_LEN),
591 self.text_index_len
592 .saturating_sub(TEXT_INDEX_PARTIAL_DIGEST_LEN),
593 )
594 } else {
595 (self.text_index_offset, self.text_index_len)
596 }
597 }
598
599 pub fn section(&self, kind: SectionKind) -> Option<Section> {
603 let (offset, length) = match kind {
604 SectionKind::Metadata => (self.metadata_offset, self.metadata_len),
605 SectionKind::Dictionary => (self.dictionary_offset, self.dictionary_len),
606 SectionKind::Index => (self.root_dir_offset, self.root_dir_len),
607 SectionKind::PyramidMeta => (self.pyramid_meta_offset, self.pyramid_meta_len),
608 SectionKind::NamedGraphs => (self.named_graphs_offset, self.named_graphs_len),
609 SectionKind::TextIndex if !self.text_index_partial => {
610 (self.text_index_offset, self.text_index_len)
611 }
612 SectionKind::TextIndexPartial if self.text_index_partial => {
613 (self.text_index_offset, self.text_index_len)
614 }
615 SectionKind::TextIndex | SectionKind::TextIndexPartial => (0, 0),
616 SectionKind::BuildInfo => (self.build_info_offset, self.build_info_len),
617 SectionKind::LabelIndex
618 | SectionKind::Schema
619 | SectionKind::QuadIndex
620 | SectionKind::Unknown(_) => {
621 return self.extra_sections.iter().find(|s| s.kind == kind).copied()
622 }
623 };
624 Some(Section {
625 kind,
626 flags: 0,
627 offset,
628 length,
629 })
630 }
631
632 pub fn with_section(mut self, kind: SectionKind, offset: u64, length: u64) -> Self {
636 match kind {
637 SectionKind::Metadata => {
638 self.metadata_offset = offset;
639 self.metadata_len = length;
640 }
641 SectionKind::Dictionary => {
642 self.dictionary_offset = offset;
643 self.dictionary_len = length;
644 }
645 SectionKind::Index => {
646 self.root_dir_offset = offset;
647 self.root_dir_len = length;
648 }
649 SectionKind::PyramidMeta => {
650 self.pyramid_meta_offset = offset;
651 self.pyramid_meta_len = length;
652 }
653 SectionKind::NamedGraphs => {
654 self.named_graphs_offset = offset;
655 self.named_graphs_len = length;
656 }
657 SectionKind::TextIndex => {
658 self.text_index_offset = offset;
659 self.text_index_len = length;
660 self.text_index_partial = false;
661 }
662 SectionKind::TextIndexPartial => {
663 self.text_index_offset = offset;
664 self.text_index_len = length;
665 self.text_index_partial = true;
666 }
667 SectionKind::BuildInfo => {
668 self.build_info_offset = offset;
669 self.build_info_len = length;
670 }
671 SectionKind::LabelIndex | SectionKind::Schema | SectionKind::QuadIndex => {
672 match self.extra_sections.iter_mut().find(|s| s.kind == kind) {
674 Some(s) => {
675 s.offset = offset;
676 s.length = length;
677 }
678 None => self.extra_sections.push(Section {
679 kind,
680 flags: 0,
681 offset,
682 length,
683 }),
684 }
685 }
686 SectionKind::Unknown(_) => self.extra_sections.push(Section {
687 kind,
688 flags: 0,
689 offset,
690 length,
691 }),
692 }
693 self
694 }
695
696 pub fn has_quad_index(&self) -> bool {
698 self.flags & FLAG_QUAD_INDEX != 0
699 }
700
701 pub fn quad_index_section(&self) -> Option<Section> {
703 self.section(SectionKind::QuadIndex)
704 .filter(|s| s.length > 0)
705 }
706
707 pub fn index_container_sections(&self) -> usize {
710 self.perms.len() + usize::from(self.has_quad_index())
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 fn sample() -> Header {
719 Header {
720 version: LEGACY_FORMAT_VERSION,
721 flags: FLAG_HAS_QUADS,
722 metadata_offset: 1024,
723 metadata_len: 42,
724 dictionary_offset: 1066,
725 dictionary_len: 2048,
726 root_dir_offset: 3114,
727 root_dir_len: 256,
728 pyramid_meta_offset: 3370,
729 pyramid_meta_len: 64,
730 dict_codec: 1,
731 block_codec: 2,
732 pyramid_levels: 3,
733 id_widths: 0,
734 perms: crate::index::PermSet::ALL,
735 quad_count: 5,
736 term_count: 9,
737 content_hash: [7u8; 16],
738 named_graphs_offset: 3434,
739 named_graphs_len: 48,
740 schema_meta_len: 99,
741 text_index_offset: 0,
742 text_index_len: 0,
743 text_index_partial: false,
744 build_info_offset: 0,
745 build_info_len: 0,
746 extra_sections: Vec::new(),
747 }
748 }
749
750 #[test]
751 fn round_trip() {
752 let h = sample();
753 let bytes = h.to_bytes();
754 assert_eq!(bytes.len(), HEADER_LEN);
755 assert_eq!(&bytes[0..4], b"RETE");
756 let back = Header::from_bytes(&bytes).unwrap();
757 assert_eq!(h, back);
758 assert!(back.has_quads());
759 }
760
761 #[test]
762 fn byte_layout_matches_spec() {
763 let h = Header {
765 content_hash: [0xCC; 16],
766 quad_count: 0x99,
767 term_count: 0xAA,
768 pyramid_levels: 0xABCD,
769 id_widths: 0,
770 dict_codec: 0xA1,
771 block_codec: 0xA2,
772 schema_meta_len: 0xD00D,
773 metadata_offset: 0x11,
774 metadata_len: 0x22,
775 dictionary_offset: 0x33,
776 dictionary_len: 0x44,
777 ..sample()
778 };
779 let b = h.to_bytes();
780 let u16_at = |o: usize| u16::from_le_bytes(b[o..o + 2].try_into().unwrap());
781 let u64_at = |o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());
782
783 assert_eq!(&b[0..4], b"RETE");
785 assert_eq!(b[4], LEGACY_FORMAT_VERSION);
786 assert_eq!(b[5], FLAG_HAS_QUADS);
787 assert_eq!(u16_at(6), HEADER_LEN as u16);
788 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);
802 assert_eq!(u64_at(96), 0x33);
803 assert_eq!(u64_at(104), 0x44);
804 assert_eq!(b.len(), HEADER_LEN);
805 }
806
807 #[test]
808 fn expected_file_len_is_last_section_end_plus_footer() {
809 assert_eq!(sample().expected_file_len(), Some(3482 + 4));
811
812 let mut h = sample();
815 h.text_index_offset = 1 << 40;
816 h.text_index_len = 0;
817 assert_eq!(h.expected_file_len(), Some(3482 + 4));
818
819 let mut e = sample();
821 e.metadata_len = 0;
822 e.dictionary_len = 0;
823 e.root_dir_len = 0;
824 e.pyramid_meta_len = 0;
825 e.named_graphs_len = 0;
826 assert_eq!(e.expected_file_len(), Some(1024 + 4));
827
828 let mut c = sample();
831 c.named_graphs_offset = u64::MAX - 8;
832 c.named_graphs_len = 64;
833 assert_eq!(c.expected_file_len(), None);
834
835 let f = sample().with_section(SectionKind::Unknown(99), 1 << 20, 512);
837 assert_eq!(f.expected_file_len(), Some((1 << 20) + 512 + 4));
838 }
839
840 #[test]
841 fn rejects_bad_magic() {
842 let mut bytes = [0u8; HEADER_LEN];
843 bytes[4] = CURRENT_FORMAT_VERSION;
844 assert!(matches!(
845 Header::from_bytes(&bytes),
846 Err(HeaderError::BadMagic)
847 ));
848 }
849
850 #[test]
851 fn stable_reader_accepts_v1_baseline_and_rejects_pre_v1() {
852 let current = sample().to_bytes();
853 assert_eq!(current[4], 0x05);
854 assert_eq!(Header::from_bytes(¤t).unwrap().version, 0x05);
855
856 for old in 0x01..=0x04 {
857 let mut bytes = current;
858 bytes[4] = old;
859 let error = Header::from_bytes(&bytes).unwrap_err();
860 assert!(matches!(
861 &error,
862 HeaderError::UnsupportedVersion {
863 found,
864 min: 0x05,
865 max: 0x06
866 } if *found == old
867 ));
868 assert!(error
869 .to_string()
870 .contains("Pre-1.0 files must be rebuilt from RDF source with `rete build`"));
871 }
872
873 for unsupported in [0x00, 0x07, 0xff] {
874 let mut bytes = current;
875 bytes[4] = unsupported;
876 assert!(matches!(
877 Header::from_bytes(&bytes),
878 Err(HeaderError::UnsupportedVersion {
879 found,
880 min: 0x05,
881 max: 0x06
882 }) if found == unsupported
883 ));
884 }
885 }
886
887 #[test]
888 fn rejects_overrunning_section_count() {
889 let mut bad = sample().to_bytes();
890 bad[44..46].copy_from_slice(&9999u16.to_le_bytes());
891 assert!(matches!(
892 Header::from_bytes(&bad),
893 Err(HeaderError::BadSectionCount(9999))
894 ));
895 }
896
897 #[test]
898 fn unknown_section_survives_round_trip() {
899 let h = sample().with_section(SectionKind::Unknown(99), 4096, 512);
902 let back = Header::from_bytes(&h.to_bytes()).unwrap();
903 assert_eq!(back.extra_sections.len(), 1);
904 let s = back.section(SectionKind::Unknown(99)).unwrap();
905 assert_eq!((s.offset, s.length), (4096, 512));
906 let dict = back.section(SectionKind::Dictionary).unwrap();
908 assert_eq!(dict.offset, h.dictionary_offset);
909 assert_eq!(h, back);
910 }
911
912 #[test]
913 fn build_info_section_round_trips_and_is_optional() {
914 let plain = sample().to_bytes();
917 assert_eq!(u16::from_le_bytes(plain[44..46].try_into().unwrap()), 5);
918 assert_eq!(sample().section(SectionKind::BuildInfo).unwrap().length, 0);
919
920 let h = sample().with_section(SectionKind::BuildInfo, 1066, 300);
922 let bytes = h.to_bytes();
923 assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 6);
924 let back = Header::from_bytes(&bytes).unwrap();
925 assert_eq!(h, back);
926 let s = back.section(SectionKind::BuildInfo).unwrap();
927 assert_eq!((s.offset, s.length), (1066, 300));
928 assert!(back.extra_sections.is_empty(), "BuildInfo is a known kind");
929 let far = sample().with_section(SectionKind::BuildInfo, 1 << 21, 128);
931 assert_eq!(far.expected_file_len(), Some((1 << 21) + 128 + 4));
932 }
933
934 #[test]
935 fn label_index_and_schema_sections_are_known_extras() {
936 assert!(sample().label_index_section().is_none());
939 assert!(sample().schema_section().is_none());
940
941 let h = sample()
942 .with_section(SectionKind::LabelIndex, 8000, 300)
943 .with_section(SectionKind::Schema, 8300, 40);
944 let bytes = h.to_bytes();
945 assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 7);
946 assert_eq!(
948 u16::from_le_bytes(bytes[64 + 5 * 24..][..2].try_into().unwrap()),
949 8
950 );
951 assert_eq!(
952 u16::from_le_bytes(bytes[64 + 6 * 24..][..2].try_into().unwrap()),
953 9
954 );
955 let back = Header::from_bytes(&bytes).unwrap();
956 assert_eq!(h, back);
957 assert_eq!(back.extra_sections.len(), 2, "stored with the extras");
958 let l = back.label_index_section().unwrap();
959 assert_eq!((l.offset, l.length), (8000, 300));
960 let s = back.schema_section().unwrap();
961 assert_eq!((s.offset, s.length), (8300, 40));
962 assert_eq!(back.expected_file_len(), Some(8340 + 4));
963
964 let again = back.with_section(SectionKind::Schema, 9000, 10);
966 assert_eq!(again.extra_sections.len(), 2);
967 assert_eq!(again.schema_section().unwrap().offset, 9000);
968
969 let empty = sample().with_section(SectionKind::LabelIndex, 0, 0);
971 assert!(empty.label_index_section().is_none());
972 }
973
974 #[test]
975 fn quad_index_flag_and_section_round_trip() {
976 let plain = sample();
978 assert!(!plain.has_quad_index());
979 assert!(plain.quad_index_section().is_none());
980 assert_eq!(plain.index_container_sections(), 6);
981
982 let mut h = sample().with_section(SectionKind::QuadIndex, 7000, 900);
985 h.flags |= FLAG_QUAD_INDEX;
986 h.named_graphs_offset = 0;
987 h.named_graphs_len = 0;
988 let bytes = h.to_bytes();
989 assert_eq!(bytes[5] & 0b1000, 0b1000, "flag bit 3");
990 let n = u16::from_le_bytes(bytes[44..46].try_into().unwrap()) as usize;
991 let kinds: Vec<u16> = (0..n)
992 .map(|i| {
993 let p = SECTION_DIR_OFFSET + i * SECTION_ENTRY_LEN;
994 u16::from_le_bytes(bytes[p..p + 2].try_into().unwrap())
995 })
996 .collect();
997 assert!(kinds.contains(&10), "a kind-10 directory entry: {kinds:?}");
998 let back = Header::from_bytes(&bytes).unwrap();
999 assert_eq!(back, h);
1000 assert!(back.has_quad_index());
1001 let s = back.quad_index_section().unwrap();
1002 assert_eq!(
1003 (s.kind, s.offset, s.length),
1004 (SectionKind::QuadIndex, 7000, 900)
1005 );
1006 assert_eq!(back.index_container_sections(), 7);
1007 assert_eq!(back.expected_file_len(), Some(7000 + 900 + 4));
1008 let mut lean = back.clone();
1010 lean.perms = crate::index::PermSet::CORE;
1011 assert_eq!(lean.index_container_sections(), 4);
1012 let again = back.with_section(SectionKind::QuadIndex, 8000, 10);
1014 assert_eq!(again.extra_sections.len(), 1);
1015 assert_eq!(again.quad_index_section().unwrap().offset, 8000);
1016 }
1017
1018 #[test]
1019 fn text_index_section_round_trips_and_is_optional() {
1020 assert_eq!(
1023 u16::from_le_bytes(sample().to_bytes()[44..46].try_into().unwrap()),
1024 5
1025 );
1026 assert!(sample().section(SectionKind::TextIndex).unwrap().length == 0);
1027
1028 let h = sample().with_section(SectionKind::TextIndex, 5000, 4096);
1030 let bytes = h.to_bytes();
1031 assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 6);
1032 let back = Header::from_bytes(&bytes).unwrap();
1033 assert_eq!(h, back);
1034 let s = back.section(SectionKind::TextIndex).unwrap();
1035 assert_eq!((s.offset, s.length), (5000, 4096));
1036 assert!(back.extra_sections.is_empty(), "TextIndex is a known kind");
1037 assert!(!back.text_index_partial);
1038 }
1039
1040 #[test]
1044 fn partial_text_index_is_kind_11_and_invisible_as_kind_6() {
1045 let h = sample().with_section(SectionKind::TextIndexPartial, 5000, 4096);
1046 assert!(h.text_index_partial);
1047 let bytes = h.to_bytes();
1048 assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 6);
1049 let p = SECTION_DIR_OFFSET + 5 * SECTION_ENTRY_LEN;
1051 assert_eq!(u16::from_le_bytes(bytes[p..p + 2].try_into().unwrap()), 11);
1052 let back = Header::from_bytes(&bytes).unwrap();
1053 assert_eq!(h, back);
1054 assert!(back.text_index_partial);
1055 assert_eq!((back.text_index_offset, back.text_index_len), (5000, 4096));
1056 let s = back.section(SectionKind::TextIndexPartial).unwrap();
1057 assert_eq!((s.offset, s.length), (5000, 4096));
1058 assert_eq!(back.section(SectionKind::TextIndex).unwrap().length, 0);
1059 assert!(back.extra_sections.is_empty(), "kind 11 is known here");
1060
1061 let mut old_view = bytes;
1065 old_view[p..p + 2].copy_from_slice(&0x7777u16.to_le_bytes());
1066 let old = Header::from_bytes(&old_view).unwrap();
1067 assert_eq!(old.text_index_len, 0);
1068 assert_eq!(old.extra_sections.len(), 1);
1069 }
1070}