guard_core_engine/patterns/
pyregex.rs1use regex::Regex;
23use std::collections::HashMap;
24use std::sync::{LazyLock, Mutex};
25
26static COMPILE_CACHE: LazyLock<Mutex<HashMap<(String, bool), Regex>>> =
33 LazyLock::new(|| Mutex::new(HashMap::new()));
34
35pub struct PyRegex {
36 pub source: String,
38 re: Regex,
39}
40
41fn translate(source: &str) -> String {
42 let mut out = String::with_capacity(source.len() + 8);
43 let mut chars = source.chars().peekable();
44 let mut in_class = false;
45 while let Some(ch) = chars.next() {
46 if ch == '\\' {
47 out.push(ch);
48 if let Some(&next) = chars.peek() {
49 if next == 'Z' {
50 out.pop();
51 out.push_str("\\z");
52 } else {
53 out.push(next);
54 }
55 chars.next();
56 }
57 continue;
58 }
59 match ch {
60 '[' if !in_class => {
61 in_class = true;
62 out.push(ch);
63 }
64 ']' if in_class => {
65 in_class = false;
66 out.push(ch);
67 }
68 '$' if !in_class => {
69 out.push_str("(?:\\n?\\z)");
71 }
72 _ => out.push(ch),
73 }
74 }
75 out
76}
77
78impl PyRegex {
79 pub fn compile(source: &str, ignore_case: bool) -> Result<Self, String> {
80 let effective_case = if source.contains("(?-i:") {
81 false
85 } else {
86 ignore_case || source.starts_with("(?i)")
87 };
88 let cache_key = (source.to_owned(), effective_case);
89 if let Ok(cache) = COMPILE_CACHE.lock()
90 && let Some(re) = cache.get(&cache_key)
91 {
92 return Ok(Self {
93 source: source.to_owned(),
94 re: re.clone(),
95 });
96 }
97 let translated = translate(source);
98 let mut pattern = String::with_capacity(translated.len() + 8);
99 if effective_case && !translated.starts_with("(?i)") {
100 pattern.push_str("(?i)");
101 }
102 pattern.push_str(&translated);
103 let re = Regex::new(&pattern).map_err(|e| format!("{source}: {e}"))?;
104 if let Ok(mut cache) = COMPILE_CACHE.lock() {
105 cache.insert(cache_key, re.clone());
106 }
107 Ok(Self {
108 source: source.to_owned(),
109 re,
110 })
111 }
112
113 #[must_use]
114 pub const fn re(&self) -> &Regex {
115 &self.re
116 }
117}
118
119#[must_use]
121pub fn cp_index(haystack: &str, byte_idx: usize) -> usize {
122 haystack[..byte_idx].chars().count()
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct Candidate {
128 pub start: usize,
129 pub end: usize,
130}
131
132impl Candidate {
133 #[must_use]
134 pub const fn new(start: usize, end: usize) -> Self {
135 Self { start, end }
136 }
137
138 #[must_use]
139 pub fn text<'a>(&self, haystack: &'a str) -> &'a str {
140 &haystack[self.start..self.end]
141 }
142}
143
144#[must_use]
145pub fn find_all(re: &Regex, haystack: &str) -> Vec<Candidate> {
146 re.find_iter(haystack)
147 .map(|m| Candidate::new(m.start(), m.end()))
148 .collect()
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn translates_absolute_end_anchor() {
157 let re = PyRegex::compile(r"\A[^\n]*x\s*\Z", false).unwrap();
158 assert!(re.re().is_match("a x"));
159 assert!(!re.re().is_match("a\nx"));
160 }
161
162 #[test]
163 fn translates_python_dollar() {
164 let re = PyRegex::compile(r"(?:/|\s|$)", false).unwrap();
165 assert!(re.re().is_match("/"));
166 assert!(re.re().is_match("x\n"));
167 }
168
169 #[test]
170 fn scoped_case_sensitivity() {
171 let re = PyRegex::compile("(?i)(?-i:rO0AB)", false).unwrap();
172 assert!(re.re().is_match("rO0AB"));
173 assert!(!re.re().is_match("ro0ab"));
174 }
175}