|
| 1 | +// This file is part of the uutils diffutils package. |
| 2 | +// |
| 3 | +// For the full copyright and license information, please view the LICENSE-* |
| 4 | +// files that was distributed with this source code. |
| 5 | + |
| 6 | +//! Translation of POSIX basic regular expressions (the `grep` syntax that |
| 7 | +//! options such as `-F` take) into the syntax of the `regex` crate. |
| 8 | +//! |
| 9 | +//! In a basic regular expression, `\(`, `\)`, `\{`, `\}`, `\|`, `\+` and `\?` |
| 10 | +//! are operators while their unescaped forms are ordinary characters, `*` is |
| 11 | +//! literal where it has nothing to repeat, `^` only anchors at the start of |
| 12 | +//! the expression or of a group or alternative, and `$` only anchors at their |
| 13 | +//! end. |
| 14 | +
|
| 15 | +use regex::bytes::Regex; |
| 16 | + |
| 17 | +/// A compiled basic regular expression. Two of them are equal when their |
| 18 | +/// patterns are. |
| 19 | +#[derive(Clone, Debug)] |
| 20 | +pub struct Bre { |
| 21 | + pattern: String, |
| 22 | + regex: Regex, |
| 23 | +} |
| 24 | + |
| 25 | +impl Bre { |
| 26 | + /// Compiles `pattern`, or returns an error message describing what is |
| 27 | + /// wrong with it. |
| 28 | + pub fn new(pattern: &str) -> Result<Self, String> { |
| 29 | + let regex = Regex::new(&translate(pattern)?).map_err(|error| match error { |
| 30 | + regex::Error::CompiledTooBig(_) => TOO_BIG.to_string(), |
| 31 | + _ => "Invalid regular expression".to_string(), |
| 32 | + })?; |
| 33 | + Ok(Self { |
| 34 | + pattern: pattern.to_string(), |
| 35 | + regex, |
| 36 | + }) |
| 37 | + } |
| 38 | + |
| 39 | + pub fn is_match(&self, text: &[u8]) -> bool { |
| 40 | + self.regex.is_match(text) |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +impl PartialEq for Bre { |
| 45 | + fn eq(&self, other: &Self) -> bool { |
| 46 | + self.pattern == other.pattern |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +impl Eq for Bre {} |
| 51 | + |
| 52 | +const TOO_BIG: &str = "Regular expression too big"; |
| 53 | +/// The largest count of an interval, as in `a\{2,100\}`. |
| 54 | +const MAX_REPETITIONS: u32 = 0x7fff; |
| 55 | + |
| 56 | +/// Translates a basic regular expression into `regex` crate syntax. |
| 57 | +fn translate(pattern: &str) -> Result<String, String> { |
| 58 | + let chars: Vec<char> = pattern.chars().collect(); |
| 59 | + let mut translated = String::with_capacity(pattern.len() * 2); |
| 60 | + // Whether the previous token opens an expression, a group or an |
| 61 | + // alternative, where `^` anchors and repetition operators are literal. |
| 62 | + let mut at_expression_start = true; |
| 63 | + let mut open_groups = 0_usize; |
| 64 | + let mut index = 0; |
| 65 | + while index < chars.len() { |
| 66 | + let c = chars[index]; |
| 67 | + index += 1; |
| 68 | + let mut opens_expression = false; |
| 69 | + match c { |
| 70 | + '\\' => { |
| 71 | + let Some(&escaped) = chars.get(index) else { |
| 72 | + return Err("Trailing backslash".to_string()); |
| 73 | + }; |
| 74 | + index += 1; |
| 75 | + match escaped { |
| 76 | + '(' => { |
| 77 | + open_groups += 1; |
| 78 | + translated.push('('); |
| 79 | + opens_expression = true; |
| 80 | + } |
| 81 | + ')' => { |
| 82 | + open_groups = open_groups |
| 83 | + .checked_sub(1) |
| 84 | + .ok_or_else(|| r"Unmatched ) or \)".to_string())?; |
| 85 | + translated.push(')'); |
| 86 | + } |
| 87 | + '|' => { |
| 88 | + translated.push('|'); |
| 89 | + opens_expression = true; |
| 90 | + } |
| 91 | + '{' | '}' | '+' | '?' if at_expression_start => { |
| 92 | + push_literal(&mut translated, escaped); |
| 93 | + } |
| 94 | + '{' => index = translate_interval(&chars, index, &mut translated)?, |
| 95 | + '+' | '?' => translated.push(escaped), |
| 96 | + '<' | '>' | 'b' | 'B' | 'w' | 'W' | 's' | 'S' => { |
| 97 | + translated.push('\\'); |
| 98 | + translated.push(escaped); |
| 99 | + } |
| 100 | + '`' => translated.push_str(r"\A"), |
| 101 | + '\'' => translated.push_str(r"\z"), |
| 102 | + '1'..='9' => return Err("Invalid back reference".to_string()), |
| 103 | + _ => push_literal(&mut translated, escaped), |
| 104 | + } |
| 105 | + } |
| 106 | + '^' if at_expression_start => { |
| 107 | + translated.push('^'); |
| 108 | + opens_expression = true; |
| 109 | + } |
| 110 | + '$' if ends_expression(&chars[index..]) => translated.push('$'), |
| 111 | + '*' if at_expression_start => translated.push_str(r"\*"), |
| 112 | + '.' | '*' => translated.push(c), |
| 113 | + '[' => index = translate_bracket(&chars, index, &mut translated)?, |
| 114 | + _ => push_literal(&mut translated, c), |
| 115 | + } |
| 116 | + at_expression_start = opens_expression; |
| 117 | + } |
| 118 | + if open_groups > 0 { |
| 119 | + return Err(r"Unmatched ( or \(".to_string()); |
| 120 | + } |
| 121 | + Ok(translated) |
| 122 | +} |
| 123 | + |
| 124 | +/// Returns whether a `$` followed by `rest` ends an expression, a group or an |
| 125 | +/// alternative. |
| 126 | +fn ends_expression(rest: &[char]) -> bool { |
| 127 | + matches!(rest, [] | ['\\', ')' | '|', ..]) |
| 128 | +} |
| 129 | + |
| 130 | +/// Translates the interval whose content starts at `index` (just after the |
| 131 | +/// `\{`), and returns the index following its closing `\}`. |
| 132 | +fn translate_interval( |
| 133 | + chars: &[char], |
| 134 | + index: usize, |
| 135 | + translated: &mut String, |
| 136 | +) -> Result<usize, String> { |
| 137 | + let end = (index..chars.len().saturating_sub(1)) |
| 138 | + .find(|&end| chars[end] == '\\' && chars[end + 1] == '}') |
| 139 | + .ok_or_else(|| r"Unmatched \{".to_string())?; |
| 140 | + let content: String = chars[index..end].iter().collect(); |
| 141 | + let invalid = || r"Invalid content of \{\}".to_string(); |
| 142 | + let count = |digits: &str| -> Result<Option<u32>, String> { |
| 143 | + if digits.is_empty() { |
| 144 | + return Ok(None); |
| 145 | + } |
| 146 | + if !digits.bytes().all(|b| b.is_ascii_digit()) { |
| 147 | + return Err(invalid()); |
| 148 | + } |
| 149 | + match digits.parse::<u32>() { |
| 150 | + Ok(count) if count <= MAX_REPETITIONS => Ok(Some(count)), |
| 151 | + _ => Err(TOO_BIG.to_string()), |
| 152 | + } |
| 153 | + }; |
| 154 | + match content.split_once(',') { |
| 155 | + None => { |
| 156 | + let exact = count(&content)?.ok_or_else(invalid)?; |
| 157 | + translated.push_str(&format!("{{{exact}}}")); |
| 158 | + } |
| 159 | + Some((min, max)) => { |
| 160 | + let min = count(min)?.unwrap_or(0); |
| 161 | + match count(max)? { |
| 162 | + Some(max) if max < min => return Err(invalid()), |
| 163 | + Some(max) => translated.push_str(&format!("{{{min},{max}}}")), |
| 164 | + None => translated.push_str(&format!("{{{min},}}")), |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + Ok(end + 2) |
| 169 | +} |
| 170 | + |
| 171 | +/// Characters with a meaning in `regex` crate syntax, inside or outside of |
| 172 | +/// bracket expressions. |
| 173 | +const META_CHARACTERS: &str = r"\.+*?()|[]{}^$#&-~"; |
| 174 | + |
| 175 | +/// The names of the character classes of bracket expressions, as in |
| 176 | +/// `[[:alpha:]]`. |
| 177 | +const CLASS_NAMES: [&str; 12] = [ |
| 178 | + "alnum", "alpha", "blank", "cntrl", "digit", "graph", "lower", "print", "punct", "space", |
| 179 | + "upper", "xdigit", |
| 180 | +]; |
| 181 | + |
| 182 | +fn push_literal(translated: &mut String, c: char) { |
| 183 | + if META_CHARACTERS.contains(c) { |
| 184 | + translated.push('\\'); |
| 185 | + } |
| 186 | + translated.push(c); |
| 187 | +} |
| 188 | + |
| 189 | +/// Translates the bracket expression whose content starts at `index` (just |
| 190 | +/// after the `[`), and returns the index following its closing `]`. |
| 191 | +fn translate_bracket( |
| 192 | + chars: &[char], |
| 193 | + mut index: usize, |
| 194 | + translated: &mut String, |
| 195 | +) -> Result<usize, String> { |
| 196 | + const UNMATCHED: &str = "Unmatched [, [^, [:, [., or [="; |
| 197 | + translated.push('['); |
| 198 | + if chars.get(index) == Some(&'^') { |
| 199 | + translated.push('^'); |
| 200 | + index += 1; |
| 201 | + } |
| 202 | + let content_start = index; |
| 203 | + // The last single character member, which can start a range. |
| 204 | + let mut range_start = None; |
| 205 | + loop { |
| 206 | + let Some(&c) = chars.get(index) else { |
| 207 | + return Err(UNMATCHED.to_string()); |
| 208 | + }; |
| 209 | + // A `]` first in the list is an ordinary member. |
| 210 | + if c == ']' && index > content_start { |
| 211 | + translated.push(']'); |
| 212 | + return Ok(index + 1); |
| 213 | + } |
| 214 | + if c == '[' && matches!(chars.get(index + 1), Some(':' | '.' | '=')) { |
| 215 | + let delimiter = chars[index + 1]; |
| 216 | + let end = (index + 2..chars.len().saturating_sub(1)) |
| 217 | + .find(|&end| chars[end] == delimiter && chars[end + 1] == ']') |
| 218 | + .ok_or_else(|| UNMATCHED.to_string())?; |
| 219 | + let name: String = chars[index + 2..end].iter().collect(); |
| 220 | + if delimiter == ':' { |
| 221 | + // A character class such as `[:alpha:]`, kept verbatim. |
| 222 | + if !CLASS_NAMES.contains(&name.as_str()) { |
| 223 | + return Err("Invalid character class name".to_string()); |
| 224 | + } |
| 225 | + translated.extend(&chars[index..end + 2]); |
| 226 | + range_start = None; |
| 227 | + } else { |
| 228 | + // A collating element or equivalence class such as `[.a.]` |
| 229 | + // or `[=a=]`, which only stands for a single character here. |
| 230 | + let mut name_chars = name.chars(); |
| 231 | + let (Some(member), None) = (name_chars.next(), name_chars.next()) else { |
| 232 | + return Err("Invalid collation character".to_string()); |
| 233 | + }; |
| 234 | + push_literal(translated, member); |
| 235 | + range_start = Some(member); |
| 236 | + } |
| 237 | + index = end + 2; |
| 238 | + continue; |
| 239 | + } |
| 240 | + let is_range = c == '-' |
| 241 | + && index > content_start |
| 242 | + && chars.get(index + 1).is_some_and(|&next| next != ']'); |
| 243 | + if is_range { |
| 244 | + if let (Some(start), Some(&end)) = (range_start, chars.get(index + 1)) { |
| 245 | + if end < start { |
| 246 | + return Err("Invalid range end".to_string()); |
| 247 | + } |
| 248 | + } |
| 249 | + translated.push('-'); |
| 250 | + range_start = None; |
| 251 | + } else { |
| 252 | + // Backslashes are ordinary members of a bracket expression, and |
| 253 | + // the `regex` crate gives meaning to `[`, `&&`, `--` and `~~`. |
| 254 | + push_literal(translated, c); |
| 255 | + range_start = Some(c); |
| 256 | + } |
| 257 | + index += 1; |
| 258 | + } |
| 259 | +} |
| 260 | + |
| 261 | +#[cfg(test)] |
| 262 | +mod tests { |
| 263 | + use super::*; |
| 264 | + use pretty_assertions::assert_eq; |
| 265 | + |
| 266 | + fn matches(pattern: &str, text: &str) -> bool { |
| 267 | + Bre::new(pattern).unwrap().is_match(text.as_bytes()) |
| 268 | + } |
| 269 | + |
| 270 | + #[test] |
| 271 | + fn groups_and_alternatives() { |
| 272 | + assert!(matches(r"^ *\(sub\|fn\) ", " fn main")); |
| 273 | + assert!(matches(r"^ *\(sub\|fn\) ", "sub x")); |
| 274 | + assert!(!matches(r"^ *\(sub\|fn\) ", " let x")); |
| 275 | + // Unescaped operators of extended expressions are ordinary characters. |
| 276 | + assert!(matches(r"f(x)|y+?", "f(x)|y+?")); |
| 277 | + assert!(!matches(r"f(x)|y+?", "fx")); |
| 278 | + } |
| 279 | + |
| 280 | + #[test] |
| 281 | + fn intervals_and_repetitions() { |
| 282 | + assert!(matches(r"^ab\{2,3\}c$", "abbc")); |
| 283 | + assert!(!matches(r"^ab\{2,3\}c$", "abc")); |
| 284 | + assert!(matches(r"^x\+$", "xxx")); |
| 285 | + assert!(matches(r"^y\?z$", "z")); |
| 286 | + // `*` with nothing to repeat is literal. |
| 287 | + assert!(matches(r"*q", "a*q")); |
| 288 | + assert!(matches(r"^*q", "*q")); |
| 289 | + assert!(matches(r"\(*\)", "*")); |
| 290 | + assert!(matches(r"to*k", "tk")); |
| 291 | + } |
| 292 | + |
| 293 | + #[test] |
| 294 | + fn anchors_only_at_the_ends() { |
| 295 | + assert!(matches(r"a^b", "a^b")); |
| 296 | + assert!(matches(r"a$b", "a$b")); |
| 297 | + assert!(matches(r"end$", "the end")); |
| 298 | + assert!(!matches(r"end$", "endless")); |
| 299 | + assert!(matches(r"\(x$\|^y\)", "yes")); |
| 300 | + } |
| 301 | + |
| 302 | + #[test] |
| 303 | + fn bracket_expressions() { |
| 304 | + assert!(matches(r"^[[:alpha:]$_]", "$var")); |
| 305 | + assert!(matches(r"^[[:alpha:]$_]", "_x")); |
| 306 | + assert!(!matches(r"^[[:alpha:]$_]", " x")); |
| 307 | + assert!(matches(r"[]x]", "]")); |
| 308 | + assert!(matches(r"[^]x]", "y")); |
| 309 | + assert!(matches(r"[a\]", "\\")); |
| 310 | + assert!(matches(r"[a-c]", "b")); |
| 311 | + assert!(matches(r"[a-]", "-")); |
| 312 | + assert!(matches(r"[&&~~[]", "[")); |
| 313 | + } |
| 314 | + |
| 315 | + #[test] |
| 316 | + fn errors() { |
| 317 | + for (pattern, message) in [ |
| 318 | + ("ab\\", "Trailing backslash"), |
| 319 | + ("[ab", "Unmatched [, [^, [:, [., or [="), |
| 320 | + ("[[:alpha:]", "Unmatched [, [^, [:, [., or [="), |
| 321 | + (r"\(a\)\1", "Invalid back reference"), |
| 322 | + (r"x\(y", r"Unmatched ( or \("), |
| 323 | + (r"x\)y", r"Unmatched ) or \)"), |
| 324 | + (r"k\{4", r"Unmatched \{"), |
| 325 | + (r"k\{\}", r"Invalid content of \{\}"), |
| 326 | + (r"k\{2,z\}", r"Invalid content of \{\}"), |
| 327 | + (r"k\{5,2\}", r"Invalid content of \{\}"), |
| 328 | + (r"k\{40000\}", "Regular expression too big"), |
| 329 | + ("[[:vowel:]]", "Invalid character class name"), |
| 330 | + ("[[.ab.]]", "Invalid collation character"), |
| 331 | + ("[m-c]", "Invalid range end"), |
| 332 | + ] { |
| 333 | + assert_eq!( |
| 334 | + Bre::new(pattern).err().as_deref(), |
| 335 | + Some(message), |
| 336 | + "{pattern}" |
| 337 | + ); |
| 338 | + } |
| 339 | + } |
| 340 | + |
| 341 | + #[test] |
| 342 | + fn literal_operators_with_nothing_to_repeat() { |
| 343 | + assert!(matches(r"\{3\}", "{3}")); |
| 344 | + assert!(matches(r"\(\+x\)", "+x")); |
| 345 | + assert!(matches(r"q\{,2\}r", "r")); |
| 346 | + assert!(matches(r"^q\{2,\}$", "qqqq")); |
| 347 | + assert!(matches(r"[[.-.][=k=]]", "-")); |
| 348 | + assert!(matches(r"[[.-.][=k=]]", "k")); |
| 349 | + } |
| 350 | +} |
0 commit comments