use std::path::Path;
use rayon::prelude::*;
use regex_syntax::hir::literal::{ExtractKind, Extractor};
use super::IndexDb;
use crate::path::RelPath;
const ROW_VERSION: u8 = 1;
const HEADER_BYTES: usize = 17;
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;
#[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], size: u64, mtime_ns: i64) -> 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;
row[1..9].copy_from_slice(&size.to_le_bytes());
row[9..17].copy_from_slice(&mtime_ns.to_le_bytes());
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
}
struct Row<'a> {
size: u64,
mtime_ns: i64,
bits: &'a [u8],
}
fn parse_row(row: &[u8]) -> Option<Row<'_>> {
if row.len() <= HEADER_BYTES || row[0] != ROW_VERSION {
return None;
}
Some(Row {
size: u64::from_le_bytes(row[1..9].try_into().ok()?),
mtime_ns: i64::from_le_bytes(row[9..17].try_into().ok()?),
bits: &row[HEADER_BYTES..],
})
}
pub fn row_stamp(row: &[u8]) -> Option<(u64, i64)> {
parse_row(row).map(|r| (r.size, r.mtime_ns))
}
pub use crate::scanner_file::mtime_nanos;
#[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 {
pub fn grep_bloom_row(&self, rel: &RelPath) -> Option<fjall::Slice> {
self.grep_bloom.get(rel.as_bytes()).ok().flatten()
}
#[doc(hidden)]
pub fn drop_grep_bloom_row_for_test(&self, rel: &RelPath) {
self.grep_bloom.remove(rel.as_bytes()).expect("remove bloom row");
}
pub fn grep_bloom_current(&self, rel: &RelPath, size: u64, mtime_ns: i64) -> bool {
self.grep_bloom_row(rel)
.and_then(|row| row_stamp(&row))
.is_some_and(|stamp| stamp == (size, mtime_ns))
}
pub fn grep_skip_verdicts(&self, root: &Path, paths: &[&RelPath], needle: &Needle) -> Vec<bool> {
paths
.par_iter()
.map(|rel| {
let Some(row) = self.grep_bloom_row(rel) else {
return false;
};
let Some(row) = parse_row(&row) else {
return false;
};
if needle.passes(row.bits) {
return false;
}
std::fs::metadata(root.join(rel.to_path_buf()))
.is_ok_and(|m| m.len() == row.size && mtime_nanos(&m) == row.mtime_ns)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn row_for(text: &str) -> Vec<u8> {
build_row(text.as_bytes(), text.len() as u64, 7)
}
fn rejects(pattern: &str, text: &str) -> bool {
let needle = Needle::compile(pattern).expect("usable needle");
let row = row_for(text);
!needle.passes(parse_row(&row).expect("row").bits)
}
#[test]
fn a_present_literal_is_never_rejected() {
let text = "fn main() { let post_processing = 1; }\n".repeat(50);
assert!(!rejects("post_processing", &text));
assert!(!rejects(r"post_processing\s*=", &text));
assert!(!rejects("(?i)POST_PROCESSING", &text));
assert!(!rejects("zzz_absent|post_processing", &text));
}
#[test]
fn an_absent_literal_is_rejected() {
assert!(rejects("eq9fsw", "fn main() { println!(\"hello\"); }"));
}
#[test]
fn patterns_without_a_required_trigram_sweep_everything() {
for pattern in [".*", r"\w+", "ab", "a|bcd", "(?i)k", "^", r"\d{3}"] {
assert!(Needle::compile(pattern).is_none(), "{pattern} must not be prefiltered");
}
}
#[test]
fn case_insensitive_literals_match_every_casing() {
let needle = Needle::compile("(?i)needle").expect("expanded to case variants");
for text in ["xx NEEDLE xx", "xx needle xx", "xx NeEdLe xx"] {
assert!(needle.passes(parse_row(&row_for(text)).unwrap().bits), "{text}");
}
}
#[test]
fn unicode_literals_are_matched_on_their_utf8_bytes() {
let needle = Needle::compile("caf\u{e9}s").expect("literal");
assert!(needle.passes(parse_row(&row_for("two caf\u{e9}s here")).unwrap().bits));
}
#[test]
fn foreign_or_short_rows_do_not_parse() {
let mut row = row_for("hello world");
assert!(parse_row(&row).is_some());
row[0] = ROW_VERSION + 1;
assert!(parse_row(&row).is_none());
assert!(parse_row(&[ROW_VERSION; HEADER_BYTES]).is_none());
assert!(parse_row(&[]).is_none());
}
#[test]
fn the_stamp_round_trips() {
let row = build_row(b"abcdef", 6, -12);
assert_eq!(row_stamp(&row), Some((6, -12)));
}
#[test]
fn row_size_tracks_the_file_within_its_bounds() {
assert_eq!(build_row(b"abc", 3, 0).len(), HEADER_BYTES + MIN_BLOOM_BYTES);
let big = vec![b'x'; 8 * 1024];
assert_eq!(build_row(&big, 8192, 0).len(), HEADER_BYTES + 1024);
let huge = vec![b'x'; 4 * 1024 * 1024];
assert_eq!(build_row(&huge, 0, 0).len(), HEADER_BYTES + MAX_BLOOM_BYTES);
}
}