use super::text::{char_filter_prefix_and_suffix, TokenFilter, Tokenizer};
use super::{errors::EncryptionError, plaintext::Plaintext, IndexTerm};
use cipherstash_core::bloom_filter::{BloomFilter, BloomFilterOps, FilterKey};
use zerokms_protocol::cipherstash_config::column;
pub struct MatchIndexer {
index_key: FilterKey,
tokenizer: Tokenizer,
token_filters: Vec<TokenFilter>,
filter_opts: BloomFilterOps,
}
impl MatchIndexer {
pub fn new(
index_key: [u8; 32],
tokenizer: column::Tokenizer,
token_filters: Vec<column::TokenFilter>,
k: usize,
m: usize,
) -> Self {
Self {
index_key,
tokenizer: tokenizer.into(),
token_filters: token_filters.into_iter().map(|v| v.into()).collect(),
filter_opts: BloomFilter::opts()
.with_filter_size(m as u32)
.with_hash_function_count(k),
}
}
pub fn encrypt(&self, plaintext: &Plaintext) -> Result<IndexTerm, EncryptionError> {
match plaintext {
Plaintext::Utf8Str(Some(value)) => {
let filtered_output = char_filter_prefix_and_suffix(value.as_str(), &['%', '_']);
let tokens = self.tokenizer.process(filtered_output);
let terms = self
.token_filters
.iter()
.fold(tokens, |tokens, filter| filter.process(tokens));
let mut filter =
BloomFilter::new(self.index_key, self.filter_opts).map_err(|e| {
EncryptionError::IndexingError(format!(
"Bloom Filter init failed with error {e}"
))
})?;
filter.add_terms(terms);
Ok(IndexTerm::BitMap(filter.into_vec()))
}
Plaintext::Utf8Str(None) => Ok(IndexTerm::Null),
_ => Err(EncryptionError::IndexingError(format!(
"{plaintext:?} is not supported by match indexes"
))),
}
}
}
impl From<column::Tokenizer> for Tokenizer {
fn from(value: column::Tokenizer) -> Self {
match value {
column::Tokenizer::Standard => Self::Standard,
column::Tokenizer::Ngram { token_length } => Self::Ngram { token_length },
column::Tokenizer::EdgeNgram { min_gram, max_gram } => {
Self::EdgeNgram { min_gram, max_gram }
}
}
}
}
impl From<column::TokenFilter> for TokenFilter {
fn from(value: column::TokenFilter) -> Self {
match value {
column::TokenFilter::Downcase => TokenFilter::Downcase,
column::TokenFilter::Upcase => TokenFilter::Upcase,
column::TokenFilter::Stemmer => TokenFilter::Stemmer,
column::TokenFilter::Stop => TokenFilter::Stop,
}
}
}
#[cfg(test)]
mod tests {
use column::{ColumnConfig, Index};
use zerokms_protocol::cipherstash_config::operator;
use super::*;
#[test]
fn test_encrypt_term() -> Result<(), Box<dyn std::error::Error>> {
let config = ColumnConfig::build("name").add_index(Index::new_match());
let index = config
.index_for_operator(&operator::Operator::Like)
.unwrap();
if let column::IndexType::Match {
tokenizer,
token_filters,
k,
m,
..
} = &index.index_type
{
let index_key = [0u8; 32];
let indexer =
MatchIndexer::new(index_key, tokenizer.clone(), token_filters.to_vec(), *k, *m);
let term = indexer.encrypt(&"Dan Draper".into())?;
assert!(matches!(term, IndexTerm::BitMap(_)));
} else {
panic!()
}
Ok(())
}
}