From 528e35c14e5d31a8f49a0d8eac83997d23395eb1 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Fri, 23 Oct 2026 05:03:01 +0530 Subject: [PATCH] fix(tinyredact): keep a partial overlap's tail redacted dedupe_overlaps dropped any hit that overlapped an earlier kept hit, so when two detectors matched overlapping ranges (0..10 and 5..15) the bytes 10..15 were left in clear text. Merge the overlapping hit into the kept one instead: the earlier, longer hit keeps its token and the combined range is redacted once. --- crates/tinyredact/src/pii/mod.rs | 12 +++++++----- crates/tinyredact/src/pii/mod_tests.rs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/tinyredact/src/pii/mod.rs b/crates/tinyredact/src/pii/mod.rs index eaa4e111..7e64e191 100644 --- a/crates/tinyredact/src/pii/mod.rs +++ b/crates/tinyredact/src/pii/mod.rs @@ -535,8 +535,10 @@ fn cc_keyword_near(norm: &str, start: usize, end: usize) -> bool { CC_KEYWORD_RE.is_match(&norm[lo..hi]) } -// Sort by start asc, length desc. Then walk in order, dropping any hit whose -// range overlaps a kept hit. Result: earlier + longer wins; no double-redact. +// Sort by start asc, length desc. Then walk in order, merging any hit whose +// range overlaps a kept hit into it. Result: earlier + longer keeps its token, +// and a partial overlap's tail is still covered (fails closed); no +// double-redact. fn dedupe_overlaps(hits: &mut Vec) { hits.sort_by(|a, b| { a.start @@ -545,9 +547,9 @@ fn dedupe_overlaps(hits: &mut Vec) { }); let mut kept: Vec = Vec::with_capacity(hits.len()); for h in hits.drain(..) { - let overlaps = kept.last().is_some_and(|k| h.start < k.end); - if !overlaps { - kept.push(h); + match kept.last_mut() { + Some(k) if h.start < k.end => k.end = k.end.max(h.end), + _ => kept.push(h), } } *hits = kept; diff --git a/crates/tinyredact/src/pii/mod_tests.rs b/crates/tinyredact/src/pii/mod_tests.rs index 1699905a..7cd023df 100644 --- a/crates/tinyredact/src/pii/mod_tests.rs +++ b/crates/tinyredact/src/pii/mod_tests.rs @@ -709,3 +709,22 @@ fn a_separated_digit_list_with_no_card_is_left_alone() { assert_eq!(redact_pii_with(input, policy).value, input, "{policy:?}"); } } + +#[test] +fn a_partial_overlap_keeps_its_tail_redacted() { + let mut hits = vec![ + Hit { + start: 5, + end: 15, + token: PII_PHONE, + }, + Hit { + start: 0, + end: 10, + token: PII_CC, + }, + ]; + dedupe_overlaps(&mut hits); + let ranges: Vec<_> = hits.iter().map(|h| (h.start, h.end, h.token)).collect(); + assert_eq!(ranges, vec![(0, 15, PII_CC)]); +}