use std::path::Path;
use rayon::prelude::*;
use regex_syntax::hir::literal::{ExtractKind, Extractor};
use super::IndexDb;
use super::view::{IndexBytes, IndexView};
use crate::path::RelPath;
const ROW_VERSION: u8 = 2;
const HEADER_BYTES: usize = 1;
const BYTES_DIVISOR: usize = 8;
const MIN_BLOOM_BYTES: usize = 64;
const MAX_BLOOM_BYTES: usize = 256 * 1024;
const HASHES: u64 = 2;
const MIN_LITERAL_BYTES: usize = 3;
const META_ROW_SWEPT_VER: &[u8] = b"grep_bloom_swept_ver";
#[inline]
fn mix(trigram: u32) -> u64 {
let mut x = u64::from(trigram).wrapping_mul(0x9E37_79B9_7F4A_7C15);
x ^= x >> 32;
x = x.wrapping_mul(0xD6E8_FEB8_6659_FD93);
x ^ (x >> 32)
}
#[inline]
fn position(mixed: u64, i: u64, nbits: u64) -> u64 {
let h1 = mixed & 0xFFFF_FFFF;
let h2 = (mixed >> 32) | 1;
((h1.wrapping_add(i.wrapping_mul(h2)) & 0xFFFF_FFFF) * nbits) >> 32
}
#[inline]
fn trigram(w: &[u8]) -> u32 {
(u32::from(w[0]) << 16) | (u32::from(w[1]) << 8) | u32::from(w[2])
}
pub fn build_row(bytes: &[u8]) -> Vec<u8> {
let bloom_bytes = (bytes.len() / BYTES_DIVISOR).clamp(MIN_BLOOM_BYTES, MAX_BLOOM_BYTES);
let nbits = (bloom_bytes * 8) as u64;
let mut row = vec![0u8; HEADER_BYTES + bloom_bytes];
row[0] = ROW_VERSION;
let bits = &mut row[HEADER_BYTES..];
for w in bytes.windows(3) {
let mixed = mix(trigram(w));
for i in 0..HASHES {
let p = position(mixed, i, nbits);
bits[(p >> 3) as usize] |= 1 << (p & 7);
}
}
row
}
fn parse_row(row: &[u8]) -> Option<&[u8]> {
(row.len() > HEADER_BYTES && row[0] == ROW_VERSION).then(|| &row[HEADER_BYTES..])
}
pub use crate::scanner_file::mtime_nanos;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct GrepEntry {
pub rel: RelPath,
pub hash_hex: String,
pub size_bytes: u64,
pub mtime_ns: i64,
}
#[derive(Debug)]
pub struct Needle {
groups: Vec<Vec<Vec<u64>>>,
}
impl Needle {
pub fn compile(pattern: &str) -> Option<Self> {
let hir = regex_syntax::ParserBuilder::new().build().parse(pattern).ok()?;
let groups: Vec<Vec<Vec<u64>>> = [ExtractKind::Prefix, ExtractKind::Suffix]
.into_iter()
.filter_map(|kind| {
let mut extractor = Extractor::new();
extractor.kind(kind);
let seq = extractor.extract(&hir);
let literals = seq.literals()?;
if literals.is_empty() || literals.iter().any(|l| l.as_bytes().len() < MIN_LITERAL_BYTES) {
return None;
}
Some(
literals
.iter()
.map(|l| {
let mut hashes: Vec<u64> = l.as_bytes().windows(3).map(|w| mix(trigram(w))).collect();
hashes.sort_unstable();
hashes.dedup();
hashes
})
.collect(),
)
})
.collect();
(!groups.is_empty()).then_some(Self { groups })
}
fn passes(&self, bits: &[u8]) -> bool {
let nbits = (bits.len() * 8) as u64;
let has = |mixed: u64| {
(0..HASHES).all(|i| {
let p = position(mixed, i, nbits);
bits[(p >> 3) as usize] & (1 << (p & 7)) != 0
})
};
self.groups
.iter()
.all(|group| group.iter().any(|alt| alt.iter().all(|&h| has(h))))
}
}
impl IndexDb {
#[doc(hidden)]
#[cfg(any(test, feature = "test-support"))]
pub fn drop_grep_bloom_row_for_test(&self, hash_hex: &str) {
self.grep_bloom.remove(hash_hex.as_bytes()).expect("remove bloom row");
}
#[doc(hidden)]
#[cfg(any(test, feature = "test-support"))]
pub fn grep_bloom_row_count(&self) -> usize {
self.grep_bloom.iter().count()
}
pub fn grep_bloom_current(&self, hash_hex: &str) -> bool {
self.view()
.grep_bloom_row(hash_hex)
.is_some_and(|row| parse_row(&row).is_some())
}
pub fn sweep_grep_bloom(&self, live: &ahash::AHashSet<&str>) -> Result<usize, super::IndexError> {
let versioned = self
.meta
.get(META_ROW_SWEPT_VER)?
.is_some_and(|v| v.first() == Some(&ROW_VERSION));
let mut writer = self.writer();
let mut dropped = 0;
for guard in self.grep_bloom.iter() {
let (key, current) = if versioned {
(guard.key()?, true)
} else {
let (key, row) = guard.into_inner()?;
(key, parse_row(&row).is_some())
};
if current && std::str::from_utf8(&key).is_ok_and(|hash| live.contains(hash)) {
continue;
}
writer.remove_grep_bloom(&key);
dropped += 1;
}
writer.commit()?;
if !versioned {
self.meta.insert(META_ROW_SWEPT_VER, [ROW_VERSION])?;
}
Ok(dropped)
}
pub fn remove_grep_bloom_rows<'a>(
&self,
hashes: impl IntoIterator<Item = &'a str>,
) -> Result<(), super::IndexError> {
let mut writer = self.writer();
for hash in hashes {
writer.remove_grep_bloom(hash.as_bytes());
}
writer.commit()
}
}
pub(super) fn skip_verdicts(
row_of: impl Fn(&str) -> Option<IndexBytes> + Sync,
root: &Path,
entries: &[GrepEntry],
needle: &Needle,
) -> Vec<bool> {
entries
.par_iter()
.map(|entry| {
let Some(row) = row_of(&entry.hash_hex) else {
return false;
};
let Some(bits) = parse_row(&row) else {
return false;
};
if needle.passes(bits) {
return false;
}
entry.mtime_ns != 0
&& std::fs::metadata(root.join(entry.rel.to_path_buf()))
.is_ok_and(|m| m.len() == entry.size_bytes && mtime_nanos(&m) == entry.mtime_ns)
})
.collect()
}
#[cfg(test)]
#[path = "grep_bloom_tests.rs"]
mod tests;