Bloom filters
Bloom filters saved to a file: have we seen this email, URL or leaked password before, for millions of items in a few megabytes, never a false no.
Version 1.0.0 · 6 functions · 1 crate
Functions
What use bloom.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use bloom_create, bloom_open from bloom (Quillo 0.6).
bloom_create(path: str, expected_items: int, false_positive_rate: float) → intor an errorMakes an empty filter at path (its folders too, replacing any filter there) for up to expected_items items (up to 100 million) with a false "maybe" rate of false_positive_rate (0.01 is 1 in 100). Gives its size in bytes: about 1.2 MB for a million items at 1%.
bloom_open(path: str, expected_items: int, false_positive_rate: float) → boolor an errorThe filter at path when there is one, else a new one made as bloom_create makes it: true when it was just made, so the caller fills it once.
bloom_add(path: str, items: list) → intor an errorAdds items (up to 100,000 a call) to the filter at path and saves it; gives how many were new (not already "maybe" in it).
bloom_check(path: str, item: str) → boolor an errorWhether item may be in the filter: false means certainly not; true means probably (wrong at the rate the filter was made for).
bloom_check_many(path: str, items: list) → listor an errorbloom_check for each of items, in order.
bloom_info(path: str) → anyor an errorThe filter's { bytes, hashes }: its size and how many hash functions each item sets.
Get it
$ quillo install bloom # bloom.qwer into this folder$ quillo install bloom --example # and its example, bloom.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/bloom.qwer$ curl -O https://quillolang.org/plugins/files/bloom.qweThen, in your program, all of it or only what you call:
use bloom.qweruse bloom_create, bloom_open from bloom // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
bloomfilter = "3"Example
// A sign-up form that refuses passwords from a leaked list, kept as a 12 KB Bloom filter.//// quillo run bloom.qwe then open http://localhost:8080/password?p=123456use "bloom.qwer"let FILTER = "data/leaked-passwords.bloom"fn load_filter() { if bloom_open(FILTER, 10000, 0.01)? { bloom_add(FILTER, ["123456", "password", "qwerty", "111111", "letmein", "iloveyou", "dragon", "monkey"])? }}route GET /password ? { p: str } { load_filter() { leaked: bloom_check(FILTER, p)? }}route GET /batch { load_filter() { checks: bloom_check_many(FILTER, ["qwerty", "correct horse battery staple", "dragon"])?, info: bloom_info(FILTER)?, again: bloom_add(FILTER, ["qwerty"])? }}$ quillo run bloom.qwe$ curl 'localhost:8080/password?p=123456'$ curl 'localhost:8080/batch'Source
//! Bloom filters saved to a file: "have we seen this before?" for millions of emails, URLs or leaked passwords in a few megabytes, never a false "no", rarely a false "maybe".//! version: 1.0.0//!//! [dependencies]//! bloomfilter = "3"use bloomfilter::Bloom;use serde_json::{json, Value};use std::collections::HashMap;use std::sync::Mutex;/// Filters in use, by path, kept in memory between calls.static OPEN: Mutex<Option<HashMap<String, Bloom<str>>>> = Mutex::new(None);fn save(path: &str, b: &Bloom<str>) -> Result<(), String> { if let Some(dir) = std::path::Path::new(path).parent() { if !dir.as_os_str().is_empty() { std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; } } // Written aside, then moved over the old file: a crash never leaves half a filter. let tmp = format!("{}.tmp", path); std::fs::write(&tmp, b.to_bytes()).map_err(|e| format!("{}: {}", tmp, e))?; std::fs::rename(&tmp, path).map_err(|e| format!("{}: {}", path, e))}fn with<T>(path: &str, f: impl FnOnce(&mut Bloom<str>) -> T) -> Result<T, String> { let mut g = OPEN.lock().unwrap_or_else(|e| e.into_inner()); let open = g.get_or_insert_with(HashMap::new); if !open.contains_key(path) { let bytes = std::fs::read(path).map_err(|e| format!("{}: {} (make it with bloom_create)", path, e))?; open.insert(path.to_string(), Bloom::from_bytes(bytes).map_err(|e| format!("{}: {}", path, e))?); } Ok(f(open.get_mut(path).ok_or("filter missing")?))}/// Makes an empty filter at `path` (its folders too, replacing any filter there) for up to `expected_items` items (up to 100 million) with a false "maybe" rate of `false_positive_rate` (0.01 is 1 in 100). Gives its size in bytes: about 1.2 MB for a million items at 1%.pub fn bloom_create(path: &str, expected_items: i64, false_positive_rate: f64) -> Result<i64, String> { if !(1..=100_000_000).contains(&expected_items) { return Err("expected_items from 1 to 100 million".into()); } if !(0.000_001..0.5).contains(&false_positive_rate) { return Err("false_positive_rate from 0.000001 to 0.5".into()); } let b: Bloom<str> = Bloom::new_for_fp_rate(expected_items as usize, false_positive_rate).map_err(|e| e.to_string())?; save(path, &b)?; let n = b.as_slice().len() as i64; OPEN.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).insert(path.to_string(), b); Ok(n)}/// The filter at `path` when there is one, else a new one made as `bloom_create` makes it: true when it was just made, so the caller fills it once.pub fn bloom_open(path: &str, expected_items: i64, false_positive_rate: f64) -> Result<bool, String> { if std::path::Path::new(path).exists() { with(path, |_| ())?; return Ok(false); } bloom_create(path, expected_items, false_positive_rate)?; Ok(true)}/// Adds `items` (up to 100,000 a call) to the filter at `path` and saves it; gives how many were new (not already "maybe" in it).pub fn bloom_add(path: &str, items: Vec<String>) -> Result<i64, String> { if items.len() > 100_000 { return Err("up to 100,000 items a call".into()); } let (n, snapshot) = with(path, |b| { let n = items.iter().filter(|i| !b.check_and_set(i.as_str())).count() as i64; (n, b.to_bytes()) })?; if n > 0 { let b = Bloom::<str>::from_bytes(snapshot).map_err(|e| e.to_string())?; save(path, &b)?; } Ok(n)}/// Whether `item` may be in the filter: false means certainly not; true means probably (wrong at the rate the filter was made for).pub fn bloom_check(path: &str, item: &str) -> Result<bool, String> { with(path, |b| b.check(item))}/// `bloom_check` for each of `items`, in order.pub fn bloom_check_many(path: &str, items: Vec<String>) -> Result<Vec<bool>, String> { with(path, |b| items.iter().map(|i| b.check(i.as_str())).collect())}/// The filter's `{ bytes, hashes }`: its size and how many hash functions each item sets.pub fn bloom_info(path: &str) -> Result<Value, String> { with(path, |b| json!({ "bytes": b.as_slice().len(), "hashes": b.number_of_hash_functions() }))}Related
file_type.qwer · 1.0.0What a file really is from its first bytes, not its name: MIME type, extension and kind, for upload checks.7 views · 9 downloadsArgon2 passwords argon2.qwer · 1.0.0Argon2id password hashes with the memory and passes you choose, any Argon2 hash verified, its parameters read, old ones marked for a rehash, and the cost timed on this server.7 views · 6 downloadsUpload checks upload_guard.qwer · 1.0.0Uploads checked before they are kept: the real type by magic bytes against the name and an allow list, size, double extensions, programs, zip bombs and zip slips, SVGs and PDFs that run scripts, macros and polyglots.7 views · 7 downloadsContent-addressed files blob_store.qwer · 1.0.0Uploads kept in a folder by their SHA-256, as Git keeps objects: the same file stored once, names pointing at contents, atomic writes, every file verified against its hash, and contents nothing names swept away.6 views · 7 downloadsThread 0 posts
On Bloom filters: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.