use std::convert::TryInto;
pub const MAGIC: [u8; 4] = *b"RETE";
pub const CURRENT_FORMAT_VERSION: u8 = 0x05;
pub const MIN_STABLE_READ_VERSION: u8 = 0x05;
pub const HEADER_LEN: usize = 1024;
const SECTION_DIR_OFFSET: usize = 64;
const SECTION_ENTRY_LEN: usize = 24;
pub const MAX_SECTIONS: usize = (HEADER_LEN - SECTION_DIR_OFFSET) / SECTION_ENTRY_LEN;
pub const FLAG_HAS_QUADS: u8 = 0b0000_0001;
pub const FLAG_TILE_SYNOPSIS: u8 = 0b0000_0010;
pub const FLAG_HAS_QUOTED_TRIPLES: u8 = 0b0000_0100;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectionKind {
Metadata,
Dictionary,
Index,
PyramidMeta,
NamedGraphs,
TextIndex,
Unknown(u16),
}
impl SectionKind {
fn to_u16(self) -> u16 {
match self {
SectionKind::Metadata => 1,
SectionKind::Dictionary => 2,
SectionKind::Index => 3,
SectionKind::PyramidMeta => 4,
SectionKind::NamedGraphs => 5,
SectionKind::TextIndex => 6,
SectionKind::Unknown(k) => k,
}
}
fn from_u16(k: u16) -> Self {
match k {
1 => SectionKind::Metadata,
2 => SectionKind::Dictionary,
3 => SectionKind::Index,
4 => SectionKind::PyramidMeta,
5 => SectionKind::NamedGraphs,
6 => SectionKind::TextIndex,
other => SectionKind::Unknown(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Section {
pub kind: SectionKind,
pub flags: u16,
pub offset: u64,
pub length: u64,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum HeaderError {
#[error("buffer too small: need {HEADER_LEN} bytes, got {0}")]
TooSmall(usize),
#[error("bad magic: expected RETE")]
BadMagic,
#[error(
"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`"
)]
UnsupportedVersion { found: u8, min: u8, max: u8 },
#[error("section count {0} overruns the header frame")]
BadSectionCount(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Header {
pub version: u8,
pub flags: u8,
pub metadata_offset: u64,
pub metadata_len: u64,
pub dictionary_offset: u64,
pub dictionary_len: u64,
pub root_dir_offset: u64,
pub root_dir_len: u64,
pub pyramid_meta_offset: u64,
pub pyramid_meta_len: u64,
pub dict_codec: u8,
pub block_codec: u8,
pub pyramid_levels: u16,
pub quad_count: u64,
pub term_count: u64,
pub content_hash: [u8; 16],
pub named_graphs_offset: u64,
pub named_graphs_len: u64,
pub schema_meta_len: u32,
pub text_index_offset: u64,
pub text_index_len: u64,
pub extra_sections: Vec<Section>,
}
impl Header {
pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
let mut b = [0u8; HEADER_LEN];
b[0..4].copy_from_slice(&MAGIC);
b[4] = self.version;
b[5] = self.flags;
b[6..8].copy_from_slice(&(HEADER_LEN as u16).to_le_bytes());
b[8..24].copy_from_slice(&self.content_hash);
b[24..32].copy_from_slice(&self.quad_count.to_le_bytes());
b[32..40].copy_from_slice(&self.term_count.to_le_bytes());
b[40..42].copy_from_slice(&self.pyramid_levels.to_le_bytes());
b[42] = self.dict_codec;
b[43] = self.block_codec;
b[46..50].copy_from_slice(&self.schema_meta_len.to_le_bytes());
let entry = |kind, offset, length| Section {
kind,
flags: 0,
offset,
length,
};
let mut entries: Vec<Section> = vec![
entry(
SectionKind::Metadata,
self.metadata_offset,
self.metadata_len,
),
entry(
SectionKind::Dictionary,
self.dictionary_offset,
self.dictionary_len,
),
entry(SectionKind::Index, self.root_dir_offset, self.root_dir_len),
entry(
SectionKind::PyramidMeta,
self.pyramid_meta_offset,
self.pyramid_meta_len,
),
entry(
SectionKind::NamedGraphs,
self.named_graphs_offset,
self.named_graphs_len,
),
];
if self.text_index_len > 0 {
entries.push(entry(
SectionKind::TextIndex,
self.text_index_offset,
self.text_index_len,
));
}
entries.extend(self.extra_sections.iter().copied());
debug_assert!(
entries.len() <= MAX_SECTIONS,
"too many sections for a 1 KB header"
);
let n = entries.len().min(MAX_SECTIONS);
b[44..46].copy_from_slice(&(n as u16).to_le_bytes());
for (i, s) in entries.iter().take(n).enumerate() {
let p = SECTION_DIR_OFFSET + i * SECTION_ENTRY_LEN;
b[p..p + 2].copy_from_slice(&s.kind.to_u16().to_le_bytes());
b[p + 2..p + 4].copy_from_slice(&s.flags.to_le_bytes());
b[p + 8..p + 16].copy_from_slice(&s.offset.to_le_bytes());
b[p + 16..p + 24].copy_from_slice(&s.length.to_le_bytes());
}
b
}
pub fn from_bytes(b: &[u8]) -> Result<Self, HeaderError> {
if b.len() < HEADER_LEN {
return Err(HeaderError::TooSmall(b.len()));
}
if b[0..4] != MAGIC {
return Err(HeaderError::BadMagic);
}
if !(MIN_STABLE_READ_VERSION..=CURRENT_FORMAT_VERSION).contains(&b[4]) {
return Err(HeaderError::UnsupportedVersion {
found: b[4],
min: MIN_STABLE_READ_VERSION,
max: CURRENT_FORMAT_VERSION,
});
}
let u16_at = |o: usize| u16::from_le_bytes(b[o..o + 2].try_into().unwrap());
let u32_at = |o: usize| u32::from_le_bytes(b[o..o + 4].try_into().unwrap());
let u64_at = |o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());
let section_count = u16_at(44) as usize;
if SECTION_DIR_OFFSET + section_count * SECTION_ENTRY_LEN > HEADER_LEN {
return Err(HeaderError::BadSectionCount(section_count));
}
let mut h = Header {
version: b[4],
flags: b[5],
metadata_offset: 0,
metadata_len: 0,
dictionary_offset: 0,
dictionary_len: 0,
root_dir_offset: 0,
root_dir_len: 0,
pyramid_meta_offset: 0,
pyramid_meta_len: 0,
dict_codec: b[42],
block_codec: b[43],
pyramid_levels: u16_at(40),
quad_count: u64_at(24),
term_count: u64_at(32),
content_hash: b[8..24].try_into().unwrap(),
named_graphs_offset: 0,
named_graphs_len: 0,
schema_meta_len: u32_at(46),
text_index_offset: 0,
text_index_len: 0,
extra_sections: Vec::new(),
};
for i in 0..section_count {
let p = SECTION_DIR_OFFSET + i * SECTION_ENTRY_LEN;
let kind = SectionKind::from_u16(u16_at(p));
let offset = u64_at(p + 8);
let length = u64_at(p + 16);
match kind {
SectionKind::Metadata => {
h.metadata_offset = offset;
h.metadata_len = length;
}
SectionKind::Dictionary => {
h.dictionary_offset = offset;
h.dictionary_len = length;
}
SectionKind::Index => {
h.root_dir_offset = offset;
h.root_dir_len = length;
}
SectionKind::PyramidMeta => {
h.pyramid_meta_offset = offset;
h.pyramid_meta_len = length;
}
SectionKind::NamedGraphs => {
h.named_graphs_offset = offset;
h.named_graphs_len = length;
}
SectionKind::TextIndex => {
h.text_index_offset = offset;
h.text_index_len = length;
}
SectionKind::Unknown(_) => h.extra_sections.push(Section {
kind,
flags: u16_at(p + 2),
offset,
length,
}),
}
}
Ok(h)
}
pub fn has_quads(&self) -> bool {
self.flags & FLAG_HAS_QUADS != 0
}
pub fn has_quoted_triples(&self) -> bool {
self.flags & FLAG_HAS_QUOTED_TRIPLES != 0
}
pub fn has_tile_synopsis(&self) -> bool {
self.flags & FLAG_TILE_SYNOPSIS != 0
}
pub fn section(&self, kind: SectionKind) -> Option<Section> {
let (offset, length) = match kind {
SectionKind::Metadata => (self.metadata_offset, self.metadata_len),
SectionKind::Dictionary => (self.dictionary_offset, self.dictionary_len),
SectionKind::Index => (self.root_dir_offset, self.root_dir_len),
SectionKind::PyramidMeta => (self.pyramid_meta_offset, self.pyramid_meta_len),
SectionKind::NamedGraphs => (self.named_graphs_offset, self.named_graphs_len),
SectionKind::TextIndex => (self.text_index_offset, self.text_index_len),
SectionKind::Unknown(_) => {
return self.extra_sections.iter().find(|s| s.kind == kind).copied()
}
};
Some(Section {
kind,
flags: 0,
offset,
length,
})
}
pub fn with_section(mut self, kind: SectionKind, offset: u64, length: u64) -> Self {
match kind {
SectionKind::Metadata => {
self.metadata_offset = offset;
self.metadata_len = length;
}
SectionKind::Dictionary => {
self.dictionary_offset = offset;
self.dictionary_len = length;
}
SectionKind::Index => {
self.root_dir_offset = offset;
self.root_dir_len = length;
}
SectionKind::PyramidMeta => {
self.pyramid_meta_offset = offset;
self.pyramid_meta_len = length;
}
SectionKind::NamedGraphs => {
self.named_graphs_offset = offset;
self.named_graphs_len = length;
}
SectionKind::TextIndex => {
self.text_index_offset = offset;
self.text_index_len = length;
}
SectionKind::Unknown(_) => self.extra_sections.push(Section {
kind,
flags: 0,
offset,
length,
}),
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Header {
Header {
version: CURRENT_FORMAT_VERSION,
flags: FLAG_HAS_QUADS,
metadata_offset: 1024,
metadata_len: 42,
dictionary_offset: 1066,
dictionary_len: 2048,
root_dir_offset: 3114,
root_dir_len: 256,
pyramid_meta_offset: 3370,
pyramid_meta_len: 64,
dict_codec: 1,
block_codec: 2,
pyramid_levels: 3,
quad_count: 5,
term_count: 9,
content_hash: [7u8; 16],
named_graphs_offset: 3434,
named_graphs_len: 48,
schema_meta_len: 99,
text_index_offset: 0,
text_index_len: 0,
extra_sections: Vec::new(),
}
}
#[test]
fn round_trip() {
let h = sample();
let bytes = h.to_bytes();
assert_eq!(bytes.len(), HEADER_LEN);
assert_eq!(&bytes[0..4], b"RETE");
let back = Header::from_bytes(&bytes).unwrap();
assert_eq!(h, back);
assert!(back.has_quads());
}
#[test]
fn byte_layout_matches_spec() {
let h = Header {
content_hash: [0xCC; 16],
quad_count: 0x99,
term_count: 0xAA,
pyramid_levels: 0xABCD,
dict_codec: 0xA1,
block_codec: 0xA2,
schema_meta_len: 0xD00D,
metadata_offset: 0x11,
metadata_len: 0x22,
dictionary_offset: 0x33,
dictionary_len: 0x44,
..sample()
};
let b = h.to_bytes();
let u16_at = |o: usize| u16::from_le_bytes(b[o..o + 2].try_into().unwrap());
let u64_at = |o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());
assert_eq!(&b[0..4], b"RETE");
assert_eq!(b[4], CURRENT_FORMAT_VERSION);
assert_eq!(b[5], FLAG_HAS_QUADS);
assert_eq!(u16_at(6), HEADER_LEN as u16);
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);
assert_eq!(u64_at(96), 0x33);
assert_eq!(u64_at(104), 0x44);
assert_eq!(b.len(), HEADER_LEN);
}
#[test]
fn rejects_bad_magic() {
let mut bytes = [0u8; HEADER_LEN];
bytes[4] = CURRENT_FORMAT_VERSION;
assert!(matches!(
Header::from_bytes(&bytes),
Err(HeaderError::BadMagic)
));
}
#[test]
fn stable_reader_accepts_v1_baseline_and_rejects_pre_v1() {
let current = sample().to_bytes();
assert_eq!(current[4], 0x05);
assert_eq!(Header::from_bytes(¤t).unwrap().version, 0x05);
for old in 0x01..=0x04 {
let mut bytes = current;
bytes[4] = old;
let error = Header::from_bytes(&bytes).unwrap_err();
assert!(matches!(
&error,
HeaderError::UnsupportedVersion {
found,
min: 0x05,
max: 0x05
} if *found == old
));
assert!(error
.to_string()
.contains("Pre-1.0 files must be rebuilt from RDF source with `rete build`"));
}
for unsupported in [0x00, 0x06, 0xff] {
let mut bytes = current;
bytes[4] = unsupported;
assert!(matches!(
Header::from_bytes(&bytes),
Err(HeaderError::UnsupportedVersion {
found,
min: 0x05,
max: 0x05
}) if found == unsupported
));
}
}
#[test]
fn rejects_overrunning_section_count() {
let mut bad = sample().to_bytes();
bad[44..46].copy_from_slice(&9999u16.to_le_bytes());
assert!(matches!(
Header::from_bytes(&bad),
Err(HeaderError::BadSectionCount(9999))
));
}
#[test]
fn unknown_section_survives_round_trip() {
let h = sample().with_section(SectionKind::Unknown(99), 4096, 512);
let back = Header::from_bytes(&h.to_bytes()).unwrap();
assert_eq!(back.extra_sections.len(), 1);
let s = back.section(SectionKind::Unknown(99)).unwrap();
assert_eq!((s.offset, s.length), (4096, 512));
let dict = back.section(SectionKind::Dictionary).unwrap();
assert_eq!(dict.offset, h.dictionary_offset);
assert_eq!(h, back);
}
#[test]
fn text_index_section_round_trips_and_is_optional() {
assert_eq!(
u16::from_le_bytes(sample().to_bytes()[44..46].try_into().unwrap()),
5
);
assert!(sample().section(SectionKind::TextIndex).unwrap().length == 0);
let h = sample().with_section(SectionKind::TextIndex, 5000, 4096);
let bytes = h.to_bytes();
assert_eq!(u16::from_le_bytes(bytes[44..46].try_into().unwrap()), 6);
let back = Header::from_bytes(&bytes).unwrap();
assert_eq!(h, back);
let s = back.section(SectionKind::TextIndex).unwrap();
assert_eq!((s.offset, s.length), (5000, 4096));
assert!(back.extra_sections.is_empty(), "TextIndex is a known kind");
}
}