use super::{Dictionary, DictionaryView, WideDictionary};
use crate::core::types::{MAX_TOKEN_SIZE, Token};
use crate::core::validate::InvalidColumn;
pub trait DictionaryStorage<D> {
fn bytes(&self) -> &[u8];
fn offsets(&self) -> &[D];
}
#[derive(Debug, Clone)]
pub struct OwnedDictionaryStorage {
bytes: Vec<u8>,
offsets: Vec<u32>,
}
impl OwnedDictionaryStorage {
pub fn new(bytes: Vec<u8>, offsets: Vec<u32>) -> Self {
Self { bytes, offsets }
}
pub fn into_raw(self) -> (Vec<u8>, Vec<u32>) {
(self.bytes, self.offsets)
}
}
impl DictionaryStorage<u32> for OwnedDictionaryStorage {
#[inline]
fn bytes(&self) -> &[u8] {
&self.bytes
}
#[inline]
fn offsets(&self) -> &[u32] {
&self.offsets
}
}
const MAX_NUM_TOKENS: usize = Token::MAX as usize + 1;
pub(crate) fn pad_raw(bytes: &mut Vec<u8>, offsets: &[u32]) {
if offsets.len() < 2 {
return;
}
let last_token_start = offsets[offsets.len() - 2] as usize;
let required = last_token_start
.checked_add(MAX_TOKEN_SIZE)
.expect("dictionary padding length must fit in usize");
if bytes.len() < required {
bytes.resize(required, 0);
}
}
fn validate_safety(bytes: &[u8], offsets: &[u32]) -> Result<(), InvalidColumn> {
let Some(num_tokens) = offsets.len().checked_sub(1) else {
return Err(InvalidColumn::EmptyDictionary);
};
if num_tokens == 0 {
return Err(InvalidColumn::EmptyDictionary);
}
if num_tokens > MAX_NUM_TOKENS {
return Err(InvalidColumn::CodeOutOfRange);
}
if offsets.first().copied() != Some(0) {
return Err(InvalidColumn::FirstOffsetNotZero);
}
let starts = &offsets[..num_tokens];
let ends = &offsets[1..];
let mut bad_decreasing = 0u32;
let mut bad_empty = 0u32;
let mut bad_length = 0u32;
for (&start, &end) in starts.iter().zip(ends) {
let length = end.wrapping_sub(start);
bad_decreasing |= (end < start) as u32;
bad_empty |= (end == start) as u32;
bad_length |= (length > MAX_TOKEN_SIZE as u32) as u32;
}
if bad_decreasing != 0 {
return Err(InvalidColumn::DecreasingOffsets);
}
if bad_empty != 0 {
return Err(InvalidColumn::EmptyToken);
}
if bad_length != 0 {
return Err(InvalidColumn::TokenTooLarge);
}
let last_start =
usize::try_from(offsets[num_tokens - 1]).map_err(|_| InvalidColumn::MissingPadding)?;
let Some(last_read_end) = last_start.checked_add(MAX_TOKEN_SIZE) else {
return Err(InvalidColumn::MissingPadding);
};
if last_read_end > bytes.len() {
return Err(InvalidColumn::MissingPadding);
}
Ok(())
}
fn validate_conformance(bytes: &[u8], offsets: &[u32]) -> Result<(), InvalidColumn> {
let mut seen = [0u64; 4];
let mut prev: &[u8] = &[];
for (&start, &end) in offsets[..offsets.len() - 1].iter().zip(&offsets[1..]) {
let token = &bytes[start as usize..end as usize];
if prev >= token {
return Err(InvalidColumn::UnsortedTokens);
}
if token.len() == 1 {
let byte = token[0] as usize;
seen[byte >> 6] |= 1u64 << (byte & 63);
}
prev = token;
}
if seen != [u64::MAX; 4] {
return Err(InvalidColumn::IncompleteAlphabet);
}
Ok(())
}
pub fn code_bits_for_num_tokens(num_tokens: usize) -> u8 {
debug_assert!(
num_tokens >= 1,
"log2(0) is undefined; num_tokens must be >= 1"
);
if num_tokens <= 1 {
1
} else {
((num_tokens as u32 - 1).ilog2() + 1) as u8
}
}
#[derive(Debug, Clone)]
pub struct CompactDictionary<S = OwnedDictionaryStorage> {
storage: S,
}
impl<S> CompactDictionary<S>
where
S: DictionaryStorage<u32>,
{
#[inline]
pub fn num_tokens(&self) -> usize {
self.storage.offsets().len().saturating_sub(1)
}
#[inline]
pub fn bytes(&self) -> &[u8] {
self.storage.bytes()
}
#[inline]
pub fn offsets(&self) -> &[u32] {
self.storage.offsets()
}
#[inline]
pub fn storage(&self) -> &S {
&self.storage
}
#[inline]
pub fn into_storage(self) -> S {
self.storage
}
#[inline]
pub fn logical_len(&self) -> usize {
self.storage.offsets().last().copied().unwrap_or(0) as usize
}
#[inline]
pub fn code_bits(&self) -> u8 {
code_bits_for_num_tokens(self.num_tokens())
}
#[inline]
pub fn to_wide(&self) -> WideDictionary {
self.as_view().to_wide()
}
pub fn validate_safety(storage: S) -> Result<Self, InvalidColumn> {
validate_safety(storage.bytes(), storage.offsets())?;
Ok(Self { storage })
}
pub fn validate(storage: S) -> Result<Self, InvalidColumn> {
let dictionary = Self::validate_safety(storage)?;
dictionary.check_correctness()?;
Ok(dictionary)
}
pub fn check_correctness(&self) -> Result<(), InvalidColumn> {
validate_conformance(self.storage.bytes(), self.storage.offsets())
}
pub unsafe fn new_unchecked(storage: S) -> Self {
Self { storage }
}
}
impl CompactDictionary<OwnedDictionaryStorage> {
#[inline]
pub fn into_raw(self) -> (Vec<u8>, Vec<u32>) {
self.storage.into_raw()
}
#[inline]
pub(crate) fn from_raw(bytes: Vec<u8>, offsets: Vec<u32>) -> Self {
Self {
storage: OwnedDictionaryStorage::new(bytes, offsets),
}
}
}
impl<S> Dictionary for CompactDictionary<S>
where
S: DictionaryStorage<u32>,
{
type View<'a>
= CompactDictionaryView<'a>
where
S: 'a;
#[inline]
fn as_view(&self) -> CompactDictionaryView<'_> {
CompactDictionaryView {
bytes: self.storage.bytes(),
offsets: self.storage.offsets(),
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct CompactDictionaryView<'a> {
bytes: &'a [u8],
offsets: &'a [u32],
}
impl<'a> CompactDictionaryView<'a> {
#[inline]
pub(crate) fn from_raw(bytes: &'a [u8], offsets: &'a [u32]) -> Self {
Self { bytes, offsets }
}
pub fn validate_safety(bytes: &'a [u8], offsets: &'a [u32]) -> Result<Self, InvalidColumn> {
validate_safety(bytes, offsets)?;
Ok(Self::from_raw(bytes, offsets))
}
pub fn validate(bytes: &'a [u8], offsets: &'a [u32]) -> Result<Self, InvalidColumn> {
validate_safety(bytes, offsets)?;
validate_conformance(bytes, offsets)?;
Ok(Self::from_raw(bytes, offsets))
}
pub fn check_correctness(&self) -> Result<(), InvalidColumn> {
validate_conformance(self.bytes, self.offsets)
}
pub unsafe fn new_unchecked(bytes: &'a [u8], offsets: &'a [u32]) -> Self {
Self::from_raw(bytes, offsets)
}
#[inline]
pub fn code_bits(&self) -> u8 {
code_bits_for_num_tokens(self.num_tokens())
}
pub fn to_wide(&self) -> WideDictionary {
let n = self.num_tokens();
let mut data = vec![0u8; n * MAX_TOKEN_SIZE];
let mut lens = vec![0u8; n];
let src = self.bytes.as_ptr();
let dst = data.as_mut_ptr();
for id in 0..n {
let (off, end) = unsafe {
(
*self.offsets.get_unchecked(id) as usize,
*self.offsets.get_unchecked(id + 1) as usize,
)
};
unsafe { *lens.get_unchecked_mut(id) = (end - off) as u8 };
unsafe {
std::ptr::copy_nonoverlapping(
src.add(off),
dst.add(id * MAX_TOKEN_SIZE),
MAX_TOKEN_SIZE,
);
}
}
WideDictionary::from_raw(data, lens)
}
}
impl DictionaryView for CompactDictionaryView<'_> {
#[inline]
fn num_tokens(&self) -> usize {
self.offsets.len().saturating_sub(1)
}
#[inline]
fn token(&self, id: Token) -> &[u8] {
let begin = self.offsets[id as usize] as usize;
let end = self.offsets[id as usize + 1] as usize;
&self.bytes[begin..end]
}
#[inline]
fn token_len(&self, id: Token) -> usize {
(self.offsets[id as usize + 1] - self.offsets[id as usize]) as usize
}
#[inline]
unsafe fn token_ptr(&self, id: Token) -> *const u8 {
unsafe {
self.bytes
.as_ptr()
.add(*self.offsets.get_unchecked(id as usize) as usize)
}
}
#[inline]
unsafe fn token_len_unchecked(&self, id: Token) -> usize {
unsafe {
(*self.offsets.get_unchecked(id as usize + 1)
- *self.offsets.get_unchecked(id as usize)) as usize
}
}
}
impl<'a> From<&'a CompactDictionary> for CompactDictionaryView<'a> {
#[inline]
fn from(d: &'a CompactDictionary) -> Self {
d.as_view()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::search::{ContainsTable, PrefixQuery, contains, starts_with, tokenize};
use crate::{DECODE_PADDING, decode_into, decoded_len, try_decode_into};
use std::mem::MaybeUninit;
use std::sync::Arc;
#[derive(Clone, Debug)]
struct SharedStorage {
bytes: Arc<[u8]>,
offsets: Arc<[u32]>,
}
impl DictionaryStorage<u32> for SharedStorage {
fn bytes(&self) -> &[u8] {
&self.bytes
}
fn offsets(&self) -> &[u32] {
&self.offsets
}
}
fn dict(offsets: Vec<u32>, bytes: &[u8]) -> CompactDictionary {
CompactDictionary::from_raw(bytes.to_vec(), offsets)
}
#[test]
fn num_tokens_is_offsets_len_minus_one() {
assert_eq!(dict(vec![0, 3, 5, 8], b"").num_tokens(), 3);
}
#[test]
fn token_returns_correct_slice() {
let d = dict(vec![0, 1, 3, 6], b"abcdef");
let v = d.as_view();
assert_eq!(v.token(0), b"a");
assert_eq!(v.token(1), b"bc");
assert_eq!(v.token(2), b"def");
assert_eq!(v.token_len(2), 3);
}
#[test]
fn storage_backed_dictionary_does_not_copy_buffers() {
let (bytes, offsets) = conformant(&[b"bc", b"def"]);
let bytes: Arc<[u8]> = bytes.into();
let offsets: Arc<[u32]> = offsets.into();
let storage = SharedStorage {
bytes: bytes.clone(),
offsets: offsets.clone(),
};
let dictionary = CompactDictionary::<SharedStorage>::validate(storage).unwrap();
assert_eq!(dictionary.bytes().as_ptr(), bytes.as_ptr());
assert_eq!(dictionary.offsets().as_ptr(), offsets.as_ptr());
assert_eq!(dictionary.as_view().token(0), &[0]);
let storage = dictionary.into_storage();
assert!(Arc::ptr_eq(&storage.bytes, &bytes));
assert!(Arc::ptr_eq(&storage.offsets, &offsets));
}
#[test]
fn code_bits_is_ceil_log2_num_tokens() {
assert_eq!(dict(vec![0; 257], b"").code_bits(), 8); assert_eq!(dict(vec![0; 258], b"").code_bits(), 9); assert_eq!(dict(vec![0; 513], b"").code_bits(), 9); assert_eq!(dict(vec![0; 514], b"").code_bits(), 10); assert_eq!(dict(vec![0; 65_537], b"").code_bits(), 16); }
#[test]
fn pad_raw_extends_to_max_token_read() {
let mut bytes = b"abc".to_vec();
pad_raw(&mut bytes, &[0, 1, 3]);
assert_eq!(bytes.len(), 1 + MAX_TOKEN_SIZE); }
#[test]
fn pad_raw_is_idempotent() {
let mut bytes = b"abc".to_vec();
let offsets = [0u32, 1, 3];
pad_raw(&mut bytes, &offsets);
let len = bytes.len();
pad_raw(&mut bytes, &offsets);
assert_eq!(bytes.len(), len);
}
#[test]
fn pad_raw_tops_up_insufficient_trailing_bytes() {
let mut bytes = vec![b'a', b'b', b'c', 0];
pad_raw(&mut bytes, &[0, 1, 3]);
assert_eq!(bytes.len(), 1 + MAX_TOKEN_SIZE);
}
#[test]
fn pad_raw_noop_for_full_width_last_token() {
let mut bytes = vec![b'z'; MAX_TOKEN_SIZE];
pad_raw(&mut bytes, &[0, MAX_TOKEN_SIZE as u32]);
assert_eq!(bytes.len(), MAX_TOKEN_SIZE);
}
fn padded(tokens: &[&[u8]]) -> (Vec<u8>, Vec<u32>) {
let mut bytes = Vec::new();
let mut offsets = vec![0u32];
for t in tokens {
bytes.extend_from_slice(t);
offsets.push(bytes.len() as u32);
}
bytes.resize(bytes.len() + MAX_TOKEN_SIZE, 0); (bytes, offsets)
}
fn assert_safe_use(tokens: &[&[u8]], text: &[u8], codes: &[Token]) {
let (bytes, offsets) = padded(tokens);
let dictionary =
CompactDictionary::validate_safety(OwnedDictionaryStorage::new(bytes, offsets))
.unwrap();
let view = dictionary.as_view();
let tokenized = tokenize(text, view);
assert_eq!(
tokenized
.iter()
.map(|&code| view.token_len(code))
.sum::<usize>(),
text.len()
);
let prefix = PrefixQuery::new(text, view);
let _ = starts_with(codes, &prefix);
let table = ContainsTable::new(text, view);
let _ = contains(codes, &table);
let expected: Vec<u8> = codes
.iter()
.flat_map(|&code| view.token(code).iter().copied())
.collect();
let decoded_len = decoded_len(codes, view);
assert_eq!(decoded_len, expected.len());
let mut padded_out = Vec::with_capacity(decoded_len + DECODE_PADDING);
let written = unsafe { decode_into(codes, view, padded_out.spare_capacity_mut()) };
unsafe { padded_out.set_len(written) };
assert_eq!(padded_out, expected);
let mut exact_out = vec![MaybeUninit::uninit(); decoded_len];
let written = try_decode_into(codes, view, &mut exact_out).unwrap();
let exact_bytes =
unsafe { std::slice::from_raw_parts(exact_out.as_ptr().cast::<u8>(), written) };
assert_eq!(exact_bytes, expected.as_slice());
}
fn conformant(extra: &[&[u8]]) -> (Vec<u8>, Vec<u32>) {
let mut toks: Vec<Vec<u8>> = (0u16..256).map(|b| vec![b as u8]).collect();
for &t in extra {
toks.push(t.to_vec());
}
toks.sort();
toks.dedup();
let refs: Vec<&[u8]> = toks.iter().map(Vec::as_slice).collect();
padded(&refs)
}
fn conformant_with_num_tokens(num_tokens: usize) -> (Vec<u8>, Vec<u32>) {
assert!((256..=256 + u16::MAX as usize + 1).contains(&num_tokens));
let mut toks: Vec<Vec<u8>> = (0u16..256).map(|b| vec![b as u8]).collect();
toks.extend((0..num_tokens - 256).map(|value| (value as u16).to_be_bytes().to_vec()));
toks.sort();
let refs: Vec<&[u8]> = toks.iter().map(Vec::as_slice).collect();
padded(&refs)
}
fn check(bytes: Vec<u8>, offsets: Vec<u32>) -> Result<(), InvalidColumn> {
CompactDictionary::validate(OwnedDictionaryStorage::new(bytes, offsets)).map(|_| ())
}
#[test]
fn validate_accepts_conformant() {
let (bytes, offsets) = conformant(&[b"bc", b"def"]);
assert_eq!(check(bytes, offsets), Ok(()));
}
#[test]
fn validate_enforces_token_address_space() {
let (bytes, offsets) = conformant_with_num_tokens(MAX_NUM_TOKENS);
assert_eq!(check(bytes, offsets), Ok(()));
let (bytes, offsets) = conformant_with_num_tokens(MAX_NUM_TOKENS + 1);
assert_eq!(check(bytes, offsets), Err(InvalidColumn::CodeOutOfRange));
}
#[test]
fn validate_classifies_safety_corruption() {
assert_eq!(
check(vec![0u8; MAX_TOKEN_SIZE + 1], vec![1, 2]),
Err(InvalidColumn::FirstOffsetNotZero)
);
let mut bytes = b"ab".to_vec();
bytes.resize(2 + MAX_TOKEN_SIZE, 0);
assert_eq!(
check(bytes, vec![0, 2, 1]),
Err(InvalidColumn::DecreasingOffsets)
);
assert_eq!(
check(vec![0u8; MAX_TOKEN_SIZE], vec![0, 0]),
Err(InvalidColumn::EmptyToken)
);
assert_eq!(
check(vec![b'x'; 20 + MAX_TOKEN_SIZE], vec![0, 20]),
Err(InvalidColumn::TokenTooLarge)
);
assert_eq!(
check(b"abc".to_vec(), vec![0, 1, 3]),
Err(InvalidColumn::MissingPadding)
);
}
#[test]
fn validate_classifies_conformance_corruption() {
let (bytes, offsets) = padded(&[&[1u8], &[0u8]]);
assert_eq!(check(bytes, offsets), Err(InvalidColumn::UnsortedTokens));
let (bytes, offsets) = padded(&[&[0u8], &[1u8], &[2u8]]);
assert_eq!(
check(bytes, offsets),
Err(InvalidColumn::IncompleteAlphabet)
);
}
#[test]
fn validate_safety_skips_conformance_checks() {
let (bytes, offsets) = padded(&[&[1u8], &[0u8]]);
let dictionary =
CompactDictionary::validate_safety(OwnedDictionaryStorage::new(bytes, offsets))
.unwrap();
assert_eq!(
dictionary.check_correctness(),
Err(InvalidColumn::UnsortedTokens)
);
let (bytes, offsets) = padded(&[&[0u8], &[1u8], &[2u8]]);
let dictionary =
CompactDictionary::validate_safety(OwnedDictionaryStorage::new(bytes, offsets))
.unwrap();
assert_eq!(
dictionary.check_correctness(),
Err(InvalidColumn::IncompleteAlphabet)
);
}
#[test]
fn validate_safety_rejects_empty_dictionary() {
assert_eq!(
CompactDictionary::validate_safety(OwnedDictionaryStorage::new(
vec![0; MAX_TOKEN_SIZE],
vec![0],
))
.map(|_| ()),
Err(InvalidColumn::EmptyDictionary)
);
}
#[test]
fn validate_safety_rejects_structural_corruption() {
let cases = [
(
vec![0u8; MAX_TOKEN_SIZE + 1],
vec![1, 2],
InvalidColumn::FirstOffsetNotZero,
),
(
vec![0u8; MAX_TOKEN_SIZE + 2],
vec![0, 2, 1],
InvalidColumn::DecreasingOffsets,
),
(
vec![0u8; MAX_TOKEN_SIZE],
vec![0, 0],
InvalidColumn::EmptyToken,
),
(
vec![0u8; MAX_TOKEN_SIZE + 17],
vec![0, 17],
InvalidColumn::TokenTooLarge,
),
(
b"abc".to_vec(),
vec![0, 1, 3],
InvalidColumn::MissingPadding,
),
];
for (bytes, offsets, expected) in cases {
assert_eq!(
CompactDictionary::validate_safety(OwnedDictionaryStorage::new(bytes, offsets))
.map(|_| ()),
Err(expected)
);
}
}
#[test]
fn safety_valid_semantically_malformed_dictionary_remains_safe_to_use() {
let (bytes, offsets) = padded(&[&[1u8], &[0u8]]);
let dictionary =
CompactDictionary::validate_safety(OwnedDictionaryStorage::new(bytes, offsets))
.unwrap();
assert_eq!(
dictionary.check_correctness(),
Err(InvalidColumn::UnsortedTokens)
);
drop(dictionary);
assert_safe_use(&[&[1u8], &[0u8]], b"xyz", &[0, 1, 0, 1]);
let (bytes, offsets) = padded(&[&[0u8], &[1u8], &[2u8]]);
let dictionary =
CompactDictionary::validate_safety(OwnedDictionaryStorage::new(bytes, offsets))
.unwrap();
assert_eq!(
dictionary.check_correctness(),
Err(InvalidColumn::IncompleteAlphabet)
);
drop(dictionary);
assert_safe_use(&[&[0u8], &[1u8], &[2u8]], b"xyz", &[2, 0, 1, 2]);
let (bytes, offsets) = padded(&[&[0u8], &[0u8]]);
let dictionary =
CompactDictionary::validate_safety(OwnedDictionaryStorage::new(bytes, offsets))
.unwrap();
assert_eq!(
dictionary.check_correctness(),
Err(InvalidColumn::UnsortedTokens)
);
drop(dictionary);
assert_safe_use(&[&[0u8], &[0u8]], b"xyz", &[0, 1, 0]);
}
#[test]
fn new_unchecked_matches_validate() {
let (bytes, offsets) = conformant(&[b"bc"]);
let checked = CompactDictionary::validate(OwnedDictionaryStorage::new(
bytes.clone(),
offsets.clone(),
))
.unwrap();
let trusted = unsafe {
CompactDictionary::new_unchecked(OwnedDictionaryStorage::new(bytes, offsets))
};
assert_eq!(checked.bytes(), trusted.bytes());
assert_eq!(checked.offsets(), trusted.offsets());
}
#[test]
fn into_raw_returns_buffers_and_round_trips() {
let (bytes, offsets) = conformant(&[b"bc", b"def"]);
let num_tokens = offsets.len() - 1;
let dict = CompactDictionary::validate(OwnedDictionaryStorage::new(
bytes.clone(),
offsets.clone(),
))
.unwrap();
let (raw_bytes, raw_offsets) = dict.into_raw();
assert_eq!(raw_bytes, bytes);
assert_eq!(raw_offsets, offsets);
let rebuilt =
CompactDictionary::validate(OwnedDictionaryStorage::new(raw_bytes, raw_offsets))
.unwrap();
assert_eq!(rebuilt.num_tokens(), num_tokens);
}
#[test]
fn view_validate_yields_usable_view() {
let (bytes, offsets) = conformant(&[b"bc"]);
let view = CompactDictionaryView::validate(&bytes, &offsets).unwrap();
assert_eq!(view.num_tokens(), 257); assert_eq!(view.token(0), &[0u8]);
let raw: &[u8] = b"abc";
assert!(CompactDictionaryView::validate(raw, &[0, 1, 3]).is_err());
}
}