use super::codec::{bits_for, crc32c, crc8, vbyte, LogSequence};
pub(crate) const BLOCK_SIZE: u64 = 16;
const TYPE_PFC: u8 = 2;
#[derive(Default)]
pub(crate) struct TermArena {
bytes: Vec<u8>,
spans: Vec<(u32, u32)>,
}
impl TermArena {
pub(crate) fn with_capacity(terms: usize, bytes: usize) -> Self {
Self {
bytes: Vec::with_capacity(bytes),
spans: Vec::with_capacity(terms),
}
}
#[cfg(test)]
pub(crate) fn push_term(&mut self, token: &str) -> usize {
let text = hdt_form(token);
self.push_bytes(&text)
}
pub(crate) fn push_bytes(&mut self, text: &[u8]) -> usize {
let start = self.bytes.len() as u32;
self.bytes.extend_from_slice(text);
self.spans.push((start, text.len() as u32));
self.spans.len() - 1
}
pub(crate) fn get(&self, i: usize) -> &[u8] {
let (start, len) = self.spans[i];
&self.bytes[start as usize..(start + len) as usize]
}
pub(crate) fn text_len(&self) -> u64 {
self.bytes.len() as u64
}
#[cfg(test)]
pub(crate) fn sorted_indices(&self) -> Vec<u32> {
let mut idx: Vec<u32> = (0..self.spans.len() as u32).collect();
idx.sort_unstable_by(|&a, &b| self.get(a as usize).cmp(self.get(b as usize)));
idx
}
}
pub(crate) fn hdt_form(token: &str) -> Vec<u8> {
if let Some(iri) = token.strip_prefix('<').and_then(|r| r.strip_suffix('>')) {
return unescape(iri);
}
if token.starts_with('"') {
if let Some(end) = literal_end(token) {
let mut out = Vec::with_capacity(token.len());
out.push(b'"');
out.extend_from_slice(&unescape(&token[1..end]));
out.push(b'"');
out.extend_from_slice(&token.as_bytes()[end + 1..]);
return out;
}
}
token.as_bytes().to_vec()
}
fn literal_end(token: &str) -> Option<usize> {
let b = token.as_bytes();
let mut i = 1;
while i < b.len() {
match b[i] {
b'\\' => i += 2,
b'"' => return Some(i),
_ => i += 1,
}
}
None
}
fn unescape(s: &str) -> Vec<u8> {
if !s.contains('\\') {
return s.as_bytes().to_vec();
}
let b = s.as_bytes();
let mut out = Vec::with_capacity(s.len());
let mut i = 0;
while i < b.len() {
if b[i] != b'\\' || i + 1 >= b.len() {
out.push(b[i]);
i += 1;
continue;
}
let (repl, used): (Option<char>, usize) = match b[i + 1] {
b't' => (Some('\t'), 2),
b'b' => (Some('\u{8}'), 2),
b'n' => (Some('\n'), 2),
b'r' => (Some('\r'), 2),
b'f' => (Some('\u{c}'), 2),
b'"' => (Some('"'), 2),
b'\'' => (Some('\''), 2),
b'\\' => (Some('\\'), 2),
b'u' => (hex_char(s, i + 2, 4), 6),
b'U' => (hex_char(s, i + 2, 8), 10),
_ => (None, 0),
};
match repl {
Some(c) => {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
i += used;
}
None => {
out.push(b[i]);
i += 1;
}
}
}
out
}
fn hex_char(s: &str, at: usize, n: usize) -> Option<char> {
let digits = s.get(at..at + n)?;
if !digits.bytes().all(|c| c.is_ascii_hexdigit()) {
return None;
}
char::from_u32(u32::from_str_radix(digits, 16).ok()?)
}
pub(crate) fn write_pfc_section(out: &mut Vec<u8>, arena: &TermArena, order: &[u32]) {
let mut text: Vec<u8> = Vec::new();
let mut blocks: Vec<u64> = Vec::with_capacity(order.len() / BLOCK_SIZE as usize + 2);
let mut prev: &[u8] = &[];
for (i, &term_idx) in order.iter().enumerate() {
let cur = arena.get(term_idx as usize);
if (i as u64).is_multiple_of(BLOCK_SIZE) {
blocks.push(text.len() as u64);
text.extend_from_slice(cur);
} else {
let delta = common_prefix(prev, cur);
vbyte(&mut text, delta as u64);
text.extend_from_slice(&cur[delta..]);
}
text.push(0);
prev = cur;
}
blocks.push(text.len() as u64);
let mut head = Vec::with_capacity(32);
head.push(TYPE_PFC);
vbyte(&mut head, order.len() as u64);
vbyte(&mut head, text.len() as u64);
vbyte(&mut head, BLOCK_SIZE);
out.extend_from_slice(&head);
out.push(crc8(&head));
LogSequence::with_bits(&blocks, bits_for(text.len() as u64)).write(out);
out.extend_from_slice(&text);
out.extend_from_slice(&crc32c(&text).to_le_bytes());
}
fn common_prefix(a: &[u8], b: &[u8]) -> usize {
let n = a.len().min(b.len());
let mut i = 0;
while i < n && a[i] == b[i] {
i += 1;
}
i
}
#[cfg(test)]
mod tests {
use super::*;
fn arena_of(terms: &[&str]) -> TermArena {
let mut a = TermArena::default();
for t in terms {
a.push_term(t);
}
a
}
#[test]
fn iris_lose_their_brackets_and_literals_keep_their_quotes() {
let a = arena_of(&[
"<http://ex/s>",
"\"Alice\"",
"\"30\"^^<http://www.w3.org/2001/XMLSchema#integer>",
"\"bonjour\"@fr",
"_:b0",
]);
assert_eq!(a.get(0), b"http://ex/s");
assert_eq!(a.get(1), b"\"Alice\"");
assert_eq!(
a.get(2),
b"\"30\"^^<http://www.w3.org/2001/XMLSchema#integer>".as_slice(),
"only the OUTER brackets of a bare IRI come off"
);
assert_eq!(a.get(3), b"\"bonjour\"@fr");
assert_eq!(a.get(4), b"_:b0");
}
#[test]
fn literals_are_stored_unescaped_with_their_structure_intact() {
let cases: &[(&str, &[u8])] = &[
(r#""line\nbreak""#, b"\"line\nbreak\""),
(r#""tab\there""#, b"\"tab\there\""),
(r#""quote\"inside""#, b"\"quote\"inside\""),
(r#""back\\slash""#, b"\"back\\slash\""),
(r#""plain"@en"#, b"\"plain\"@en"),
];
for (token, want) in cases {
assert_eq!(&hdt_form(token)[..], *want, "hdt_form({token})");
}
}
#[test]
fn a_unicode_escape_becomes_the_character() {
assert_eq!(
hdt_form(r#""uni\u00C1code""#),
"\"uni\u{c1}code\"".as_bytes()
);
assert_eq!(hdt_form(r#""\U0001F600""#), "\"\u{1F600}\"".as_bytes());
assert_eq!(
hdt_form(r"<http://ex/caf\u00E9>"),
"http://ex/caf\u{e9}".as_bytes()
);
}
#[test]
fn a_datatype_suffix_is_structural_and_survives() {
assert_eq!(
hdt_form(r#""30"^^<http://www.w3.org/2001/XMLSchema#integer>"#),
b"\"30\"^^<http://www.w3.org/2001/XMLSchema#integer>".as_slice(),
"the datatype keeps its angle brackets, unlike a bare IRI term"
);
}
#[test]
fn an_unrecognized_escape_keeps_its_backslash() {
assert_eq!(hdt_form(r#""a\qb""#), b"\"a\\qb\"".as_slice());
}
#[test]
fn blank_nodes_pass_through_untouched() {
assert_eq!(hdt_form("_:b0"), b"_:b0");
}
#[test]
fn stripping_brackets_changes_the_order() {
let rete_order = {
let mut v = vec!["<http://ex/a>", "<http://ex/a/b>"];
v.sort_unstable();
v
};
assert_eq!(rete_order, vec!["<http://ex/a/b>", "<http://ex/a>"]);
let a = arena_of(&["<http://ex/a>", "<http://ex/a/b>"]);
let order = a.sorted_indices();
assert_eq!(a.get(order[0] as usize), b"http://ex/a");
assert_eq!(a.get(order[1] as usize), b"http://ex/a/b");
}
#[test]
fn blank_nodes_and_iris_swap_blocks() {
let a = arena_of(&["<http://ex/s>", "_:b0"]);
let order = a.sorted_indices();
assert_eq!(a.get(order[0] as usize), b"_:b0");
assert_eq!(a.get(order[1] as usize), b"http://ex/s");
}
#[test]
fn a_literal_sorts_before_everything() {
let a = arena_of(&["<http://ex/s>", "_:b0", "\"Alice\""]);
let order = a.sorted_indices();
assert_eq!(a.get(order[0] as usize), b"\"Alice\"");
}
fn decode_pfc(bytes: &[u8]) -> Vec<Vec<u8>> {
let mut p = 0usize;
assert_eq!(bytes[p], TYPE_PFC);
p += 1;
let read_vbyte = |p: &mut usize| -> u64 {
let mut v = 0u64;
let mut shift = 0;
loop {
let b = bytes[*p];
*p += 1;
v |= ((b & 127) as u64) << shift;
if b & 0x80 != 0 {
return v;
}
shift += 7;
}
};
let numstrings = read_vbyte(&mut p);
let textlen = read_vbyte(&mut p) as usize;
let blocksize = read_vbyte(&mut p);
p += 1; assert_eq!(bytes[p], 0x01);
let numbits = bytes[p + 1] as u64;
p += 2;
let numentries = read_vbyte(&mut p);
p += 1; let databytes = (numbits * numentries).div_ceil(8) as usize;
p += databytes + 4; let text = &bytes[p..p + textlen];
let mut out = Vec::new();
let mut prev: Vec<u8> = Vec::new();
let mut t = 0usize;
for i in 0..numstrings {
let cur = if i % blocksize == 0 {
let end = t + text[t..].iter().position(|&b| b == 0).unwrap();
let s = text[t..end].to_vec();
t = end + 1;
s
} else {
let mut delta = 0u64;
let mut shift = 0;
loop {
let b = text[t];
t += 1;
delta |= ((b & 127) as u64) << shift;
if b & 0x80 != 0 {
break;
}
shift += 7;
}
let end = t + text[t..].iter().position(|&b| b == 0).unwrap();
let mut s = prev[..delta as usize].to_vec();
s.extend_from_slice(&text[t..end]);
t = end + 1;
s
};
prev = cur.clone();
out.push(cur);
}
out
}
#[test]
fn a_front_coded_section_decodes_back_to_its_terms() {
let terms: Vec<String> = (0..40)
.map(|i| format!("<http://dblp.example/resource/authors/Person{i:04}>"))
.collect();
let refs: Vec<&str> = terms.iter().map(String::as_str).collect();
let arena = arena_of(&refs);
let order = arena.sorted_indices();
let mut out = Vec::new();
write_pfc_section(&mut out, &arena, &order);
let got = decode_pfc(&out);
assert_eq!(got.len(), 40);
for (i, &oi) in order.iter().enumerate() {
assert_eq!(got[i], arena.get(oi as usize), "term {i}");
}
}
#[test]
fn a_section_with_no_shared_prefixes_still_round_trips() {
let arena = arena_of(&["\"zebra\"", "_:x", "<http://a/1>", "\"apple\""]);
let order = arena.sorted_indices();
let mut out = Vec::new();
write_pfc_section(&mut out, &arena, &order);
let got = decode_pfc(&out);
assert_eq!(
got,
vec![
b"\"apple\"".to_vec(),
b"\"zebra\"".to_vec(),
b"_:x".to_vec(),
b"http://a/1".to_vec(),
]
);
}
#[test]
fn an_empty_section_is_well_formed() {
let arena = TermArena::default();
let mut out = Vec::new();
write_pfc_section(&mut out, &arena, &[]);
assert_eq!(out[0], TYPE_PFC);
assert_eq!(decode_pfc(&out).len(), 0);
}
#[test]
fn the_block_sequence_has_one_entry_per_block_plus_a_sentinel() {
let terms: Vec<String> = (0..40).map(|i| format!("<http://ex/{i:04}>")).collect();
let refs: Vec<&str> = terms.iter().map(String::as_str).collect();
let arena = arena_of(&refs);
let order = arena.sorted_indices();
let mut out = Vec::new();
write_pfc_section(&mut out, &arena, &order);
let mut p = 1usize;
let read_vbyte = |p: &mut usize| -> u64 {
let mut v = 0u64;
let mut shift = 0;
loop {
let b = out[*p];
*p += 1;
v |= ((b & 127) as u64) << shift;
if b & 0x80 != 0 {
return v;
}
shift += 7;
}
};
read_vbyte(&mut p); read_vbyte(&mut p); read_vbyte(&mut p); p += 1; assert_eq!(out[p], 0x01);
p += 2; let numentries = read_vbyte(&mut p);
assert_eq!(numentries, 4, "3 blocks + 1 sentinel");
}
#[test]
fn common_prefix_is_a_byte_count_not_a_char_count() {
assert_eq!(common_prefix("café".as_bytes(), "cafx".as_bytes()), 3);
assert_eq!(common_prefix("café".as_bytes(), "café!".as_bytes()), 5);
assert_eq!(common_prefix(b"", b"abc"), 0);
}
}