use crate::core::bitvec::BitVec;
use crate::error::{BloomCraftError, Result};
use crate::filters::standard::StandardBloomFilter;
use crate::hash::hasher::BloomHasher;
use crate::hash::IndexingStrategy;
use serde::de;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const FORMAT_VERSION: u16 = 1;
#[derive(Serialize, Deserialize)]
struct StandardBloomFilterSerde {
version: u16,
size: usize,
num_hashes: usize,
hash_strategy: u8,
hasher_type: String,
bits: Vec<u64>,
num_bits_set: Option<usize>,
}
impl StandardBloomFilterSerde {
fn from_filter<T, H>(filter: &StandardBloomFilter<T, H>) -> Self
where
T: std::hash::Hash,
H: BloomHasher + Clone,
{
Self {
version: FORMAT_VERSION,
size: filter.size(),
num_hashes: filter.num_hashes(),
hash_strategy: strategy_to_id(filter.hash_strategy()),
hasher_type: filter.hasher_name().to_string(),
bits: filter.raw_bits(),
num_bits_set: Some(filter.count_set_bits()),
}
}
fn into_filter<T, H>(self) -> Result<StandardBloomFilter<T, H>>
where
T: std::hash::Hash,
H: BloomHasher + Default + Clone,
{
if self.version != FORMAT_VERSION {
return Err(BloomCraftError::invalid_parameters(format!(
"Incompatible serialization version: expected {}, got {}",
FORMAT_VERSION, self.version
)));
}
if self.size == 0 {
return Err(BloomCraftError::invalid_filter_size(self.size));
}
if self.num_hashes == 0 || self.num_hashes > 32 {
return Err(BloomCraftError::invalid_hash_count(self.num_hashes, 1, 32));
}
id_to_strategy(self.hash_strategy)?;
let expected = H::default().name();
if self.hasher_type != expected {
return Err(BloomCraftError::invalid_parameters(format!(
"Hasher type mismatch: filter was serialized with '{}', \
but the target type uses '{}'. \
Using a different hasher produces incorrect bit positions, \
causing 100% false negatives for all inserted items. \
Deserialize with the same hasher used during serialization.",
self.hasher_type, expected
)));
}
let bits = BitVec::from_raw(self.bits, self.size)?;
let filter = StandardBloomFilter::<T, H>::from_parts(bits, self.num_hashes)?;
let has_bits = self
.num_bits_set
.map_or_else(|| filter.count_set_bits() > 0, |n| n > 0);
if has_bits {
filter.mark_has_inserts();
}
Ok(filter)
}
}
fn strategy_to_id(strategy: IndexingStrategy) -> u8 {
match strategy {
IndexingStrategy::Double => 0,
IndexingStrategy::EnhancedDouble => 1,
IndexingStrategy::Triple => 2,
}
}
fn id_to_strategy(id: u8) -> Result<IndexingStrategy> {
match id {
0 => Ok(IndexingStrategy::Double),
1 => Ok(IndexingStrategy::EnhancedDouble),
2 => Ok(IndexingStrategy::Triple),
_ => Err(BloomCraftError::invalid_parameters(format!(
"Unknown hash strategy ID: {}. \
The payload may be corrupt or produced by a newer version of bloomcraft.",
id
))),
}
}
impl<T, H> Serialize for StandardBloomFilter<T, H>
where
T: std::hash::Hash,
H: BloomHasher + Clone,
{
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
StandardBloomFilterSerde::from_filter(self).serialize(serializer)
}
}
impl<'de, T, H> Deserialize<'de> for StandardBloomFilter<T, H>
where
T: std::hash::Hash,
H: BloomHasher + Default + Clone,
{
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
StandardBloomFilterSerde::deserialize(deserializer)?
.into_filter::<T, H>()
.map_err(de::Error::custom)
}
}
pub struct StandardFilterSerdeSupport;
impl StandardFilterSerdeSupport {
pub fn estimate_size(expected_items: usize, fp_rate: f64) -> Result<usize> {
use crate::core::params;
let m = params::optimal_bit_count(expected_items, fp_rate)?;
let metadata = 64;
let data = m.div_ceil(64) * 8 + 8;
Ok(metadata + data)
}
#[cfg(feature = "bincode")]
pub fn to_bytes<T, H>(filter: &StandardBloomFilter<T, H>) -> Result<Vec<u8>>
where
T: std::hash::Hash,
H: BloomHasher + Default + Clone,
{
bincode::serialize(filter)
.map_err(|e| BloomCraftError::serialization_error(e.to_string()))
}
#[cfg(feature = "bincode")]
pub fn from_bytes<T, H>(bytes: &[u8]) -> Result<StandardBloomFilter<T, H>>
where
T: std::hash::Hash,
H: BloomHasher + Default + Clone,
{
bincode::deserialize(bytes)
.map_err(|e| BloomCraftError::serialization_error(e.to_string()))
}
pub fn to_json<T, H>(filter: &StandardBloomFilter<T, H>) -> Result<String>
where
T: std::hash::Hash,
H: BloomHasher + Default + Clone,
{
serde_json::to_string(filter)
.map_err(|e| BloomCraftError::serialization_error(e.to_string()))
}
pub fn from_json<T, H>(json: &str) -> Result<StandardBloomFilter<T, H>>
where
T: std::hash::Hash,
H: BloomHasher + Default + Clone,
{
serde_json::from_str(json)
.map_err(|e| BloomCraftError::serialization_error(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::hasher::StdHasher;
#[cfg(feature = "bincode")]
#[test]
fn bincode_round_trip() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(1_000, 0.01).unwrap();
filter.insert(&"hello");
filter.insert(&"world");
let bytes = bincode::serialize(&filter).unwrap();
assert!(!bytes.is_empty());
let restored: StandardBloomFilter<&str, StdHasher> =
bincode::deserialize(&bytes).unwrap();
assert!(restored.contains(&"hello"));
assert!(restored.contains(&"world"));
assert!(!restored.contains(&"missing"));
}
#[cfg(feature = "bincode")]
#[test]
fn helper_methods_round_trip() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(1_000, 0.01).unwrap();
filter.insert(&"hello");
let bytes = StandardFilterSerdeSupport::to_bytes(&filter).unwrap();
let from_bytes: StandardBloomFilter<&str, StdHasher> =
StandardFilterSerdeSupport::from_bytes(&bytes).unwrap();
assert!(from_bytes.contains(&"hello"));
let json = StandardFilterSerdeSupport::to_json(&filter).unwrap();
let from_json: StandardBloomFilter<&str, StdHasher> =
StandardFilterSerdeSupport::from_json(&json).unwrap();
assert!(from_json.contains(&"hello"));
}
#[cfg(feature = "bincode")]
#[test]
fn empty_filter_round_trip() {
let filter = StandardBloomFilter::<String, StdHasher>::new(1_000, 0.01).unwrap();
let bytes = bincode::serialize(&filter).unwrap();
let restored: StandardBloomFilter<String, StdHasher> =
bincode::deserialize(&bytes).unwrap();
assert!(restored.is_empty());
}
#[cfg(feature = "bincode")]
#[test]
fn full_filter_round_trip() {
let filter = StandardBloomFilter::<i32, StdHasher>::new(100, 0.01).unwrap();
for i in 0..100_i32 {
filter.insert(&i);
}
let bytes = bincode::serialize(&filter).unwrap();
let restored: StandardBloomFilter<i32, StdHasher> =
bincode::deserialize(&bytes).unwrap();
for i in 0..100_i32 {
assert!(restored.contains(&i));
}
}
#[cfg(feature = "bincode")]
#[test]
fn large_filter_round_trip() {
let filter = StandardBloomFilter::<i32, StdHasher>::new(100_000, 0.001).unwrap();
for i in 0..10_000_i32 {
filter.insert(&i);
}
let bytes = bincode::serialize(&filter).unwrap();
let restored: StandardBloomFilter<i32, StdHasher> =
bincode::deserialize(&bytes).unwrap();
for i in 0..10_000_i32 {
assert!(restored.contains(&i));
}
}
#[cfg(feature = "bincode")]
#[test]
fn serialization_preserves_bit_statistics() {
let filter = StandardBloomFilter::<i32, StdHasher>::new(1_000, 0.01).unwrap();
for i in 0..100_i32 {
filter.insert(&i);
}
let original_bits = filter.count_set_bits();
let original_fpr = filter.estimate_fpr();
let bytes = bincode::serialize(&filter).unwrap();
let restored: StandardBloomFilter<i32, StdHasher> =
bincode::deserialize(&bytes).unwrap();
assert_eq!(restored.count_set_bits(), original_bits);
assert!((restored.estimate_fpr() - original_fpr).abs() < 1e-6);
}
#[cfg(feature = "bincode")]
#[test]
fn deserialized_non_empty_filter_is_not_reported_as_empty() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(1_000, 0.01).unwrap();
filter.insert(&"hello");
assert!(!filter.is_empty(), "pre-condition: source filter must not be empty");
let bytes = bincode::serialize(&filter).unwrap();
let restored: StandardBloomFilter<&str, StdHasher> =
bincode::deserialize(&bytes).unwrap();
assert!(
!restored.is_empty(),
"deserialized non-empty filter must not report is_empty() == true"
);
assert!(restored.contains(&"hello"));
}
#[cfg(feature = "bincode")]
#[test]
fn deserialized_empty_filter_is_still_empty() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(1_000, 0.01).unwrap();
assert!(filter.is_empty());
let bytes = bincode::serialize(&filter).unwrap();
let restored: StandardBloomFilter<&str, StdHasher> =
bincode::deserialize(&bytes).unwrap();
assert!(
restored.is_empty(),
"deserialized empty filter must still report is_empty() == true"
);
}
#[test]
fn json_round_trip() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(100, 0.01).unwrap();
filter.insert(&"test");
let json = serde_json::to_string(&filter).unwrap();
assert!(json.contains("version"));
assert!(json.contains("bits"));
assert!(json.contains("hasher_type"));
assert!(json.contains("StdHasher"));
let restored: StandardBloomFilter<&str, StdHasher> =
serde_json::from_str(&json).unwrap();
assert!(restored.contains(&"test"));
}
#[test]
fn hasher_mismatch_rejected() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(100, 0.01).unwrap();
filter.insert(&"test");
let mut repr = StandardBloomFilterSerde::from_filter(&filter);
repr.hasher_type = "WrongHasher".to_string();
let err = repr.into_filter::<&str, StdHasher>().unwrap_err().to_string();
assert!(err.contains("Hasher type mismatch"));
assert!(err.contains("WrongHasher"));
assert!(err.contains("StdHasher"));
}
#[test]
fn correct_hasher_accepted() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(100, 0.01).unwrap();
filter.insert(&"test");
let repr = StandardBloomFilterSerde::from_filter(&filter);
let restored = repr.into_filter::<&str, StdHasher>().unwrap();
assert!(restored.contains(&"test"));
}
#[test]
fn estimate_size_plausible() {
let estimated = StandardFilterSerdeSupport::estimate_size(10_000, 0.01).unwrap();
assert!(estimated > 1_000 && estimated < 100_000);
}
#[test]
fn known_strategy_ids_accepted() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(1_000, 0.01).unwrap();
filter.insert(&"test");
for id in [0u8, 1, 2] {
let mut repr = StandardBloomFilterSerde::from_filter(&filter);
repr.hash_strategy = id;
let restored = repr
.into_filter::<&str, StdHasher>()
.unwrap_or_else(|_| panic!("strategy id {} should be accepted", id));
assert!(
restored.contains(&"test"),
"item missing after round-trip with strategy id {}",
id
);
}
}
#[test]
fn unknown_strategy_id_rejected() {
let filter = StandardBloomFilter::<&str, StdHasher>::new(100, 0.01).unwrap();
let mut repr = StandardBloomFilterSerde::from_filter(&filter);
repr.hash_strategy = 99;
assert!(repr.into_filter::<&str, StdHasher>().is_err());
}
#[test]
fn version_mismatch_rejected() {
let repr = StandardBloomFilterSerde {
version: 99,
size: 1_000,
num_hashes: 7,
hash_strategy: 1,
hasher_type: "StdHasher".to_string(),
bits: vec![0; 16],
num_bits_set: Some(0),
};
assert!(repr.into_filter::<&str, StdHasher>().is_err());
}
#[test]
fn invalid_strategy_id_returns_error() {
assert!(id_to_strategy(99).is_err());
}
#[test]
fn strategy_id_encoding_is_stable() {
let cases = [
(IndexingStrategy::Double, 0u8),
(IndexingStrategy::EnhancedDouble, 1),
(IndexingStrategy::Triple, 2),
];
for (strategy, expected_id) in cases {
assert_eq!(strategy_to_id(strategy), expected_id);
assert_eq!(id_to_strategy(expected_id).unwrap(), strategy);
}
}
}