use crate::engine::{CompiledPolicy, Hit};
use crate::input::Prepared;
use crate::views::NormView;
use crate::Config;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
pub const HANDLED: &[&str] = &["SLOP-U001"];
const TRIGGER_CAP: usize = 120;
struct Tok {
start: usize,
end: usize,
word: usize,
seg: u32,
}
struct Tokens {
toks: Vec<Tok>,
buf: String,
}
impl Tokens {
fn word(&self, i: usize) -> &str {
let s = self.toks[i].word;
let e = self
.toks
.get(i + 1)
.map(|t| t.word)
.unwrap_or(self.buf.len());
&self.buf[s..e]
}
fn shingles_eq(&self, a: usize, b: usize, k: usize) -> bool {
(0..k).all(|d| self.word(a + d) == self.word(b + d))
}
}
fn tokenize(text: &str) -> Tokens {
let mut toks = Vec::new();
let mut buf = String::new();
let mut seg = 0u32;
let mut in_word = false;
let mut start = 0usize;
let mut word = 0usize;
for (i, c) in text.char_indices() {
let c = if c == '\u{2019}' { '\'' } else { c };
if c.is_alphanumeric() || c == '\'' {
if !in_word {
start = i;
word = buf.len();
in_word = true;
}
for lc in c.to_lowercase() {
buf.push(lc);
}
} else {
if in_word {
toks.push(Tok {
start,
end: i,
word,
seg,
});
in_word = false;
}
if c == '\u{FFFD}' {
seg += 1;
}
}
}
if in_word {
toks.push(Tok {
start,
end: text.len(),
word,
seg,
});
}
Tokens { toks, buf }
}
fn shingle_hash(tokens: &Tokens, i: usize, k: usize) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
for d in 0..k {
tokens.word(i + d).hash(&mut h);
}
h.finish()
}
pub fn evaluate(
cp: &CompiledPolicy,
prepared: &Prepared,
norm: &NormView,
config: &Config,
hits: &mut Vec<Hit>,
) {
let Some(idx) = super::active(cp, config, "SLOP-U001") else {
return;
};
let rule = &cp.pkg.rules[idx];
let k = super::param_i64(rule, "shingle_words").unwrap_or(8).max(1) as usize;
let floor = super::param_i64(rule, "min_run_words").unwrap_or(10).max(1) as usize;
let cap = super::param_i64(rule, "max_reports").unwrap_or(20).max(0) as usize;
let text = norm.text.as_str();
let src = prepared.text.as_str();
let tokens = tokenize(text);
let toks = &tokens.toks;
if toks.len() < k {
return;
}
const WALK_CAP: usize = 32;
const NIL: usize = usize::MAX;
let mut heads: HashMap<u64, usize> = HashMap::new();
let mut next: Vec<usize> = vec![NIL; toks.len()];
let mut runs: Vec<(usize, usize, usize)> = Vec::new();
let mut i = 0usize;
while i + k <= toks.len() {
if toks[i + k - 1].seg != toks[i].seg {
i += 1; continue;
}
if norm.span_has_flag(
&(toks[i].start..toks[i + k - 1].end),
crate::extract::F_QUOTED,
) {
i += 1;
continue;
}
match heads.entry(shingle_hash(&tokens, i, k)) {
Entry::Vacant(v) => {
v.insert(i);
i += 1;
}
Entry::Occupied(mut o) => {
let mut best: Option<(usize, usize, usize)> = None;
let mut e = *o.get();
let mut walked = 0usize;
loop {
if e + k <= i && tokens.shingles_eq(e, i, k) {
let mut len = k;
while i + len < toks.len()
&& e + len < i
&& tokens.word(e + len) == tokens.word(i + len)
&& toks[e + len].seg == toks[e].seg
&& toks[i + len].seg == toks[i].seg
{
len += 1;
}
let (mut es, mut s, mut len) = (e, i, len);
while es > 0
&& es + len < s
&& tokens.word(es - 1) == tokens.word(s - 1)
&& toks[es - 1].seg == toks[es].seg
&& toks[s - 1].seg == toks[s].seg
{
es -= 1;
s -= 1;
len += 1;
}
if best.is_none_or(|(_, _, b)| len > b) {
best = Some((es, s, len));
}
}
walked += 1;
if walked >= WALK_CAP || next[e] == NIL {
break;
}
e = next[e];
}
next[i] = *o.get();
o.insert(i);
match best {
Some((e, s, len)) if len >= floor => {
runs.push((e, s, len));
i = s + len;
}
_ => i += 1,
}
}
}
}
runs.sort_by_key(|&(e, s, len)| (std::cmp::Reverse(len), s, e));
runs.truncate(cap);
for (e, s, len) in runs {
let span = toks[s].start..toks[s + len - 1].end;
let earlier = toks[e].start..toks[e + len - 1].end;
if norm.all_quoted(&span) || norm.all_quoted(&earlier) {
continue;
}
let Some(source_span) = norm.to_source(span.clone()) else {
continue;
};
let source_span = crate::widen_to_char_boundaries(src, source_span);
if source_span.start >= source_span.end {
continue;
}
let mut hit = Hit::new(idx, source_span);
let mut trigger = &text[span];
if trigger.len() > TRIGGER_CAP {
let mut cut = TRIGGER_CAP;
while !trigger.is_char_boundary(cut) {
cut -= 1;
}
trigger = &trigger[..cut];
}
hit.trigger = Some(trigger.to_string());
hits.push(hit);
}
}