use std::hash::Hasher;
use xxhash_rust::xxh64::Xxh64;
use super::config::Config;
use super::node::Node;
pub fn is_boundary(node: &Node, idx: usize) -> bool {
let count = node.keys.len();
if count < node.min_chunk_size {
return false;
}
if count >= node.max_chunk_size {
return true;
}
is_hash_boundary(
node.hash_seed,
node.chunking_factor,
&node.keys[idx],
&node.vals[idx],
)
}
pub fn is_boundary_config(config: &Config, count: usize, key: &[u8], val: &[u8]) -> bool {
if count < config.min_chunk_size {
return false;
}
if count >= config.max_chunk_size {
return true;
}
is_hash_boundary_config(config, key, val)
}
pub(crate) fn is_hash_boundary_config(config: &Config, key: &[u8], val: &[u8]) -> bool {
is_hash_boundary(config.hash_seed, config.chunking_factor, key, val)
}
fn is_hash_boundary(hash_seed: u64, chunking_factor: u32, key: &[u8], val: &[u8]) -> bool {
let mut hasher = Xxh64::new(hash_seed);
hasher.write(key);
hasher.write(val);
let hash = hasher.finish();
let hash_val = (hash & 0xFFFF_FFFF) as u32;
let threshold = u32::MAX / chunking_factor;
hash_val <= threshold
}
#[cfg(test)]
mod tests {
use super::super::encoding::Encoding;
use super::*;
#[test]
fn test_is_boundary_below_min_chunk_size() {
let node = Node::builder()
.keys(vec![b"a".to_vec(), b"b".to_vec()])
.vals(vec![b"1".to_vec(), b"2".to_vec()])
.min_chunk_size(4)
.max_chunk_size(100)
.chunking_factor(128)
.build();
assert!(!is_boundary(&node, 0));
assert!(!is_boundary(&node, 1));
}
#[test]
fn test_is_boundary_at_max_chunk_size() {
let keys: Vec<Vec<u8>> = (0..10).map(|i| vec![i]).collect();
let vals: Vec<Vec<u8>> = (0..10).map(|i| vec![i]).collect();
let node = Node::builder()
.keys(keys)
.vals(vals)
.min_chunk_size(2)
.max_chunk_size(10) .chunking_factor(128)
.build();
assert!(is_boundary(&node, 0));
}
#[test]
fn test_is_boundary_deterministic() {
let node = Node::builder()
.keys(vec![
b"key1".to_vec(),
b"key2".to_vec(),
b"key3".to_vec(),
b"key4".to_vec(),
b"key5".to_vec(),
])
.vals(vec![
b"val1".to_vec(),
b"val2".to_vec(),
b"val3".to_vec(),
b"val4".to_vec(),
b"val5".to_vec(),
])
.min_chunk_size(2)
.max_chunk_size(100)
.chunking_factor(128)
.hash_seed(42)
.build();
let result1 = is_boundary(&node, 2);
let result2 = is_boundary(&node, 2);
assert_eq!(result1, result2);
}
#[test]
fn test_is_boundary_config_below_min() {
let config = Config::builder()
.min_chunk_size(4)
.max_chunk_size(100)
.chunking_factor(128)
.build();
assert!(!is_boundary_config(&config, 2, b"key", b"val"));
}
#[test]
fn test_is_boundary_config_at_max() {
let config = Config::builder()
.min_chunk_size(2)
.max_chunk_size(10)
.chunking_factor(128)
.build();
assert!(is_boundary_config(&config, 10, b"key", b"val"));
}
#[test]
fn test_is_boundary_config_deterministic() {
let config = Config::builder()
.min_chunk_size(2)
.max_chunk_size(100)
.chunking_factor(128)
.hash_seed(42)
.build();
let result1 = is_boundary_config(&config, 5, b"test_key", b"test_val");
let result2 = is_boundary_config(&config, 5, b"test_key", b"test_val");
assert_eq!(result1, result2);
}
#[test]
fn test_is_boundary_matches_is_boundary_config() {
let node = Node::builder()
.keys(vec![
b"a".to_vec(),
b"b".to_vec(),
b"c".to_vec(),
b"d".to_vec(),
b"e".to_vec(),
])
.vals(vec![
b"1".to_vec(),
b"2".to_vec(),
b"3".to_vec(),
b"4".to_vec(),
b"5".to_vec(),
])
.min_chunk_size(2)
.max_chunk_size(100)
.chunking_factor(128)
.hash_seed(42)
.encoding(Encoding::Raw)
.build();
let config = Config::builder()
.min_chunk_size(2)
.max_chunk_size(100)
.chunking_factor(128)
.hash_seed(42)
.encoding(Encoding::Raw)
.build();
for idx in 0..node.keys.len() {
let node_result = is_boundary(&node, idx);
let config_result =
is_boundary_config(&config, node.keys.len(), &node.keys[idx], &node.vals[idx]);
assert_eq!(
node_result, config_result,
"Mismatch at index {}: is_boundary={}, is_boundary_config={}",
idx, node_result, config_result
);
}
}
#[test]
fn test_different_seeds_produce_different_results() {
let config1 = Config::builder()
.min_chunk_size(2)
.max_chunk_size(100)
.chunking_factor(128)
.hash_seed(1)
.build();
let config2 = Config::builder()
.min_chunk_size(2)
.max_chunk_size(100)
.chunking_factor(128)
.hash_seed(999999)
.build();
let mut found_difference = false;
for i in 0..100 {
let key = format!("key{}", i).into_bytes();
let val = format!("val{}", i).into_bytes();
let r1 = is_boundary_config(&config1, 5, &key, &val);
let r2 = is_boundary_config(&config2, 5, &key, &val);
if r1 != r2 {
found_difference = true;
break;
}
}
assert!(
found_difference,
"Different seeds should produce different boundary patterns"
);
}
#[test]
fn test_higher_chunking_factor_fewer_boundaries() {
let config_low = Config::builder()
.min_chunk_size(2)
.max_chunk_size(1000)
.chunking_factor(4) .hash_seed(0)
.build();
let config_high = Config::builder()
.min_chunk_size(2)
.max_chunk_size(1000)
.chunking_factor(1024) .hash_seed(0)
.build();
let mut low_boundaries = 0;
let mut high_boundaries = 0;
for i in 0..1000 {
let key = format!("key{:04}", i).into_bytes();
let val = format!("val{:04}", i).into_bytes();
if is_boundary_config(&config_low, 100, &key, &val) {
low_boundaries += 1;
}
if is_boundary_config(&config_high, 100, &key, &val) {
high_boundaries += 1;
}
}
assert!(
low_boundaries > high_boundaries,
"Lower chunking factor should produce more boundaries: low={}, high={}",
low_boundaries,
high_boundaries
);
}
}