guard_core_engine/patterns/
scan_window.rs1use regex::Regex;
14
15use super::pyregex::{Candidate, PyRegex};
16
17fn match_span(re: &Regex, haystack: &str, start: usize, end: usize) -> Option<Candidate> {
20 if start > end {
21 return None;
22 }
23 let truncated = &haystack[..end];
24 let m = re.find_at(truncated, start)?;
25 if m.start() != start {
26 return None;
27 }
28 Some(Candidate::new(m.start(), m.end()))
29}
30
31#[must_use]
32pub fn bounded_finditer(
33 haystack: &str,
34 compiled: &PyRegex,
35 prefix: &PyRegex,
36 terminator: &PyRegex,
37) -> Vec<Candidate> {
38 bounded_finditer_guarded(haystack, compiled, prefix, terminator, |_, _| true)
39}
40
41#[must_use]
45pub fn bounded_finditer_guarded(
46 haystack: &str,
47 compiled: &PyRegex,
48 prefix: &PyRegex,
49 terminator: &PyRegex,
50 guard: impl Fn(&str, Candidate) -> bool,
51) -> Vec<Candidate> {
52 let terminator_ends: Vec<usize> = terminator
53 .re()
54 .find_iter(haystack)
55 .map(|m| m.end())
56 .collect();
57 if terminator_ends.is_empty() {
58 return Vec::new();
59 }
60 let ceiling = terminator_ends[terminator_ends.len() - 1];
61
62 let prefix_starts: Vec<usize> = prefix.re().find_iter(haystack).map(|m| m.start()).collect();
63 if prefix_starts.is_empty() {
64 return Vec::new();
65 }
66
67 let mut matches = Vec::new();
68 let mut search_from = 0;
69 let mut start_index = 0;
70 loop {
71 while start_index < prefix_starts.len() && prefix_starts[start_index] < search_from {
72 start_index += 1;
73 }
74 let mut found: Option<Candidate> = None;
75 for (offset, &start) in prefix_starts.iter().enumerate().skip(start_index) {
76 if start >= ceiling {
77 break;
78 }
79 if let Some(m) = match_span(compiled.re(), haystack, start, ceiling)
80 && guard(haystack, m)
81 {
82 start_index = offset;
83 found = Some(m);
84 break;
85 }
86 }
87 let Some(m) = found else {
88 return matches;
89 };
90 matches.push(m);
91 search_from = if m.end > m.start { m.end } else { m.start + 1 };
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn finds_matches_inside_window() {
101 let compiled = PyRegex::compile(r"<script[^>]*>[^<]*<\/script\s*>", false).unwrap();
102 let prefix = PyRegex::compile("<script", false).unwrap();
103 let terminator = PyRegex::compile(r"<\/script\s*>", false).unwrap();
104 let text = "junk <script>a</script> tail <script>b</script>";
105 let ms = bounded_finditer(text, &compiled, &prefix, &terminator);
106 assert_eq!(ms.len(), 2);
107 assert_eq!(ms[0].text(text), "<script>a</script>");
108 assert_eq!(ms[1].text(text), "<script>b</script>");
109 }
110
111 #[test]
112 fn no_terminator_no_match() {
113 let compiled = PyRegex::compile(r"<script[^>]*>[^<]*<\/script\s*>", false).unwrap();
114 let prefix = PyRegex::compile("<script", false).unwrap();
115 let terminator = PyRegex::compile(r"<\/script\s*>", false).unwrap();
116 assert!(bounded_finditer("<script>a", &compiled, &prefix, &terminator).is_empty());
117 }
118}
119
120#[cfg(test)]
121mod coverage_tests {
122 use super::*;
123
124 #[test]
125 fn bounded_finditer_returns_nothing_when_the_prefix_is_absent() {
126 let compiled =
127 crate::patterns::pyregex::PyRegex::compile(r"<script[^>]*>x</script>", false).unwrap();
128 let prefix = crate::patterns::pyregex::PyRegex::compile("<script", false).unwrap();
129 let terminator =
130 crate::patterns::pyregex::PyRegex::compile(r"<\/script\s*>", false).unwrap();
131 assert!(bounded_finditer("tail </script>", &compiled, &prefix, &terminator).is_empty());
133 }
134
135 #[test]
136 fn a_prefix_position_the_window_cannot_anchor_is_abandoned() {
137 let compiled = PyRegex::compile(r"<script[^>]*>[^<]*<\/script\s*>", false).unwrap();
138 let prefix = PyRegex::compile("<script", false).unwrap();
139 let terminator = PyRegex::compile(r"<\/script\s*>", false).unwrap();
140 let text = "<script>a</b> <script>b</script>";
143 let ms = bounded_finditer(text, &compiled, &prefix, &terminator);
144 assert_eq!(ms.len(), 1);
145 assert_eq!(ms[0].text(text), "<script>b</script>");
146 }
147
148 #[test]
149 fn a_prefix_window_that_cannot_complete_returns_none_and_is_abandoned() {
150 let compiled = PyRegex::compile(r"<script[^>]*>[^<]*<\/script\s*>", false).unwrap();
151 let prefix = PyRegex::compile("<script", false).unwrap();
152 let terminator = PyRegex::compile(r"<\/script\s*>", false).unwrap();
153 let text = "<script><b></script> tail";
156 assert!(bounded_finditer(text, &compiled, &prefix, &terminator).is_empty());
157 }
158
159 #[test]
160 fn a_zero_width_window_match_advances_one_char() {
161 let compiled = PyRegex::compile("z*", false).unwrap();
164 let prefix = PyRegex::compile("x", false).unwrap();
165 let terminator = PyRegex::compile("b", false).unwrap();
166 let ms = bounded_finditer("xb", &compiled, &prefix, &terminator);
167 assert_eq!(ms.len(), 1);
168 assert_eq!(ms[0].start, 0);
169 assert_eq!(ms[0].end, 0);
170 }
171
172 #[test]
173 fn prefix_candidates_at_or_past_the_last_terminator_are_skipped() {
174 let compiled = PyRegex::compile(r"<script[^>]*>[^<]*<\/script\s*>", false).unwrap();
175 let prefix = PyRegex::compile("<script", false).unwrap();
176 let terminator = PyRegex::compile(r"<\/script\s*>", false).unwrap();
177 let text = "<script>a</script> tail <script>";
179 let ms = bounded_finditer(text, &compiled, &prefix, &terminator);
180 assert_eq!(ms.len(), 1);
181 assert_eq!(ms[0].text(text), "<script>a</script>");
182 }
183}
184
185#[cfg(test)]
186mod unit_twins {
187 use super::*;
188 use crate::patterns::pyregex::PyRegex;
189
190 #[test]
191 fn an_inverted_window_is_never_anchored() {
192 let re = PyRegex::compile(r"a", false).unwrap();
194 assert!(match_span(re.re(), "abc", 2, 1).is_none());
195 }
196}