1use super::{to_f64, value_to_string, Result, SQLError, TemporalValue, Value};
10
11pub(super) fn typeof_value(v: &Value) -> String {
16 match v {
17 Value::Null => "null".into(),
18 Value::Void => "void".into(),
19 Value::Bool(_) => "boolean".into(),
20 Value::Int(_) => "integer".into(),
21 Value::Float(_) => "double precision".into(),
22 Value::Decimal(_) => "numeric".into(),
23 Value::Str(_) => "text".into(),
24 Value::FixedChar(_) => "character".into(),
25 Value::Bytes(_) => "bytea".into(),
26 Value::Temporal(value) => match value {
27 TemporalValue::Date { .. } => "date".into(),
28 TemporalValue::Time { .. } => "time without time zone".into(),
29 TemporalValue::TimeTz { .. } => "time with time zone".into(),
30 TemporalValue::Timestamp { .. } => "timestamp without time zone".into(),
31 TemporalValue::TimestampTz { .. } => "timestamp with time zone".into(),
32 TemporalValue::Interval { .. } => "interval".into(),
33 },
34 Value::Json(_) => "json".into(),
35 Value::JsonB(_) => "jsonb".into(),
36 Value::Array(_) => "array".into(),
37 Value::List(_) => "array".into(),
38 Value::Row(_) | Value::Record(_) => "record".into(),
39 Value::Map(_) => "jsonb".into(),
40 }
41}
42
43pub(super) fn point_xy(v: &Value) -> Result<(f64, f64)> {
44 match v {
45 Value::List(items) if items.len() == 2 => Ok((to_f64(&items[0])?, to_f64(&items[1])?)),
46 Value::Str(s) | Value::FixedChar(s) => {
47 let cleaned = s.trim_matches(|c: char| c == '(' || c == ')' || c == '[' || c == ']');
48 let parts: Vec<&str> = cleaned.split(',').map(str::trim).collect();
49 if parts.len() != 2 {
50 return Err(SQLError::TypeMismatch(format!("point: cannot parse {s:?}")));
51 }
52 let x: f64 = parts[0]
53 .parse()
54 .map_err(|e| SQLError::TypeMismatch(format!("point.x: {e}")))?;
55 let y: f64 = parts[1]
56 .parse()
57 .map_err(|e| SQLError::TypeMismatch(format!("point.y: {e}")))?;
58 Ok((x, y))
59 }
60 other => Err(SQLError::TypeMismatch(format!(
61 "point: not coercible {other:?}"
62 ))),
63 }
64}
65
66pub struct CompiledLikePattern {
71 case_insensitive: bool,
72 pattern_chars: Vec<LikePatternToken<char>>,
73 pattern_ascii: Option<Vec<LikePatternToken<u8>>>,
74}
75
76#[derive(Clone, Copy, PartialEq, Eq)]
77enum LikePatternToken<T> {
78 Literal(T),
79 AnyOne,
80 AnyMany,
81 DanglingEscape,
82}
83
84impl CompiledLikePattern {
85 #[must_use]
86 pub fn new(pattern: &str, case_insensitive: bool) -> Self {
87 Self::with_escape(pattern, case_insensitive, None)
88 .expect("the default LIKE escape is exactly one character")
89 }
90
91 #[must_use]
92 pub fn from_value(pattern: &Value, case_insensitive: bool) -> Self {
93 Self::new(&value_to_string(pattern), case_insensitive)
94 }
95
96 pub fn with_escape(
99 pattern: &str,
100 case_insensitive: bool,
101 escape: Option<&str>,
102 ) -> Result<Self> {
103 let escape = like_escape_character(escape)?;
104 let pattern_chars = compile_like_pattern(pattern, case_insensitive, escape);
105 let pattern_ascii = pattern_chars
106 .iter()
107 .map(|token| match token {
108 LikePatternToken::Literal(character) if character.is_ascii() => {
109 Some(LikePatternToken::Literal(*character as u8))
110 }
111 LikePatternToken::Literal(_) => None,
112 LikePatternToken::AnyOne => Some(LikePatternToken::AnyOne),
113 LikePatternToken::AnyMany => Some(LikePatternToken::AnyMany),
114 LikePatternToken::DanglingEscape => Some(LikePatternToken::DanglingEscape),
115 })
116 .collect::<Option<Vec<_>>>();
117 Ok(Self {
118 case_insensitive,
119 pattern_chars,
120 pattern_ascii,
121 })
122 }
123
124 #[must_use]
125 pub fn is_match(&self, haystack: &str) -> bool {
126 self.try_is_match(haystack).unwrap_or(false)
127 }
128
129 pub fn try_is_match(&self, haystack: &str) -> Result<bool> {
131 if self.case_insensitive {
132 let normalized = haystack.to_lowercase();
133 if let Some(pattern) = self
134 .pattern_ascii
135 .as_deref()
136 .filter(|_| normalized.is_ascii())
137 {
138 return wildcard_match(normalized.as_bytes(), pattern);
139 }
140 let haystack = normalized.chars().collect::<Vec<_>>();
141 return wildcard_match(&haystack, &self.pattern_chars);
142 }
143 if let Some(pattern) = self
144 .pattern_ascii
145 .as_deref()
146 .filter(|_| haystack.is_ascii())
147 {
148 return wildcard_match(haystack.as_bytes(), pattern);
149 }
150 let haystack = haystack.chars().collect::<Vec<_>>();
151 wildcard_match(&haystack, &self.pattern_chars)
152 }
153
154 #[must_use]
155 pub fn matches_value(&self, haystack: &Value) -> bool {
156 self.try_matches_value(haystack).unwrap_or(false)
157 }
158
159 pub fn try_matches_value(&self, haystack: &Value) -> Result<bool> {
161 match haystack {
162 Value::Str(text) => self.try_is_match(text),
163 Value::FixedChar(text) => self.try_is_match(text.trim_end_matches(' ')),
164 Value::Null => self.try_is_match(""),
165 other => self.try_is_match(&value_to_string(other)),
166 }
167 }
168}
169
170fn like_escape_character(escape: Option<&str>) -> Result<Option<char>> {
171 let Some(escape) = escape else {
172 return Ok(Some('\\'));
173 };
174 let mut characters = escape.chars();
175 let first = characters.next();
176 if characters.next().is_some() {
177 return Err(SQLError::Routine {
178 sqlstate: "22025".into(),
179 message: "invalid escape string".into(),
180 });
181 }
182 Ok(first)
183}
184
185fn compile_like_pattern(
186 pattern: &str,
187 case_insensitive: bool,
188 escape: Option<char>,
189) -> Vec<LikePatternToken<char>> {
190 let mut output = Vec::with_capacity(pattern.chars().count());
191 let mut characters = pattern.chars();
192 while let Some(character) = characters.next() {
193 if escape == Some(character) {
194 let Some(literal) = characters.next() else {
195 output.push(LikePatternToken::DanglingEscape);
196 break;
197 };
198 push_like_literal(&mut output, literal, case_insensitive);
199 continue;
200 }
201 match character {
202 '%' => output.push(LikePatternToken::AnyMany),
203 '_' => output.push(LikePatternToken::AnyOne),
204 literal => push_like_literal(&mut output, literal, case_insensitive),
205 }
206 }
207 output
208}
209
210fn push_like_literal(
211 output: &mut Vec<LikePatternToken<char>>,
212 literal: char,
213 case_insensitive: bool,
214) {
215 if case_insensitive {
216 output.extend(literal.to_lowercase().map(LikePatternToken::Literal));
217 } else {
218 output.push(LikePatternToken::Literal(literal));
219 }
220}
221
222fn wildcard_match<T: Copy + Eq>(haystack: &[T], pattern: &[LikePatternToken<T>]) -> Result<bool> {
223 let mut haystack_index = 0;
224 let mut pattern_index = 0;
225 let mut star: Option<(usize, usize)> = None;
226 while haystack_index < haystack.len() {
227 match pattern.get(pattern_index) {
228 Some(LikePatternToken::Literal(literal)) if *literal == haystack[haystack_index] => {
229 haystack_index += 1;
230 pattern_index += 1;
231 }
232 Some(LikePatternToken::AnyOne) => {
233 haystack_index += 1;
234 pattern_index += 1;
235 }
236 Some(LikePatternToken::AnyMany) => {
237 star = Some((pattern_index, haystack_index));
238 pattern_index += 1;
239 }
240 Some(LikePatternToken::DanglingEscape) => {
241 return Err(SQLError::Routine {
242 sqlstate: "22025".into(),
243 message: "LIKE pattern must not end with escape character".into(),
244 });
245 }
246 _ => {
247 if let Some((star_pattern, star_haystack)) = star {
248 pattern_index = star_pattern + 1;
249 haystack_index = star_haystack + 1;
250 star = Some((star_pattern, star_haystack + 1));
251 } else {
252 return Ok(false);
253 }
254 }
255 }
256 }
257 while matches!(pattern.get(pattern_index), Some(LikePatternToken::AnyMany)) {
258 pattern_index += 1;
259 }
260 Ok(pattern_index == pattern.len())
261}
262
263pub(super) fn trim_chars(args: &[Value], start: bool, end: bool) -> Result<Value> {
266 if args.is_empty() || args.len() > 2 {
267 return Err(SQLError::TypeMismatch("trim takes 1-2 args".into()));
268 }
269 if args.iter().any(|arg| matches!(arg, Value::Null)) {
270 return Ok(Value::Null);
271 }
272 let s = value_to_string(&args[0]);
273 let out = match args.get(1) {
274 None => match (start, end) {
275 (true, true) => s.trim(),
276 (true, false) => s.trim_start(),
277 (false, true) => s.trim_end(),
278 (false, false) => s.as_str(),
279 }
280 .to_string(),
281 Some(set) => {
282 let set: Vec<char> = value_to_string(set).chars().collect();
283 let matches_set = |c: char| set.contains(&c);
284 let mut out = s.as_str();
285 if start {
286 out = out.trim_start_matches(matches_set);
287 }
288 if end {
289 out = out.trim_end_matches(matches_set);
290 }
291 out.to_string()
292 }
293 };
294 Ok(Value::Str(out))
295}
296
297pub(super) fn compile_pg_regex(
299 pattern: &str,
300 flags: &str,
301 global_allowed: bool,
302) -> Result<regex::Regex> {
303 #[derive(Clone, Copy)]
304 enum Syntax {
305 Advanced,
306 Basic,
307 Quoted,
308 }
309
310 let mut case_insensitive = false;
311 let mut multi_line = false;
312 let mut dot_matches_new_line = true;
313 let mut expanded = false;
314 let mut syntax = Syntax::Advanced;
315 for flag in flags.chars() {
316 match flag {
317 'g' if global_allowed => {}
318 'b' | 'e' => syntax = Syntax::Basic,
322 'c' => case_insensitive = false,
323 'i' => case_insensitive = true,
324 'm' | 'n' => {
325 multi_line = true;
326 dot_matches_new_line = false;
327 }
328 'p' => {
329 multi_line = false;
330 dot_matches_new_line = false;
331 }
332 'q' => syntax = Syntax::Quoted,
333 's' => {
334 multi_line = false;
335 dot_matches_new_line = true;
336 }
337 't' => expanded = false,
338 'w' => {
339 multi_line = true;
340 dot_matches_new_line = true;
341 }
342 'x' => expanded = true,
343 invalid => {
344 return Err(SQLError::Routine {
345 sqlstate: "22023".into(),
346 message: format!("invalid regular expression option: \"{invalid}\""),
347 });
348 }
349 }
350 }
351 if matches!(syntax, Syntax::Quoted) && (expanded || multi_line || !dot_matches_new_line) {
352 return Err(SQLError::Routine {
353 sqlstate: "2201B".into(),
354 message: "invalid regular expression: invalid argument to regex function".into(),
355 });
356 }
357 let pattern = if expanded {
358 expand_postgres_regex(pattern)
359 } else {
360 pattern.to_string()
361 };
362 let pattern = match syntax {
363 Syntax::Advanced => pattern,
364 Syntax::Basic => postgres_basic_regex(&pattern),
365 Syntax::Quoted => regex::escape(&pattern),
366 };
367 let pattern = postgres_character_class_regex(&pattern, !dot_matches_new_line);
368 let mut builder = regex::RegexBuilder::new(&pattern);
369 builder
370 .case_insensitive(case_insensitive)
371 .multi_line(multi_line)
372 .dot_matches_new_line(dot_matches_new_line);
373 builder.build().map_err(|error| SQLError::Routine {
374 sqlstate: "2201B".into(),
375 message: format!("invalid regular expression: {error}"),
376 })
377}
378
379fn postgres_character_class_regex(pattern: &str, exclude_newline: bool) -> String {
380 let characters = pattern.chars().collect::<Vec<_>>();
381 let mut output = String::with_capacity(pattern.len());
382 let mut position = 0usize;
383 let mut in_bracket = false;
384 let mut bracket_can_close = false;
385 while let Some(&character) = characters.get(position) {
386 position += 1;
387 if character == '\\' {
388 output.push(character);
389 if let Some(&escaped) = characters.get(position) {
390 position += 1;
391 output.push(escaped);
392 if in_bracket {
393 bracket_can_close = true;
394 }
395 }
396 continue;
397 }
398 if !in_bracket {
399 output.push(character);
400 if character == '[' {
401 in_bracket = true;
402 bracket_can_close = false;
403 if characters.get(position) == Some(&'^') {
404 position += 1;
405 output.push('^');
406 if characters.get(position) == Some(&']') {
407 position += 1;
408 output.push(']');
409 bracket_can_close = true;
410 }
411 if exclude_newline {
412 output.push_str("\\n");
413 if characters.get(position) == Some(&'-') {
414 position += 1;
415 output.push_str("\\-");
416 bracket_can_close = true;
417 }
418 }
419 }
420 }
421 continue;
422 }
423 if character == '[' && matches!(characters.get(position), Some('.' | ':' | '=')) {
424 let delimiter = characters[position];
425 output.push(character);
426 output.push(delimiter);
427 position += 1;
428 while let Some(&nested) = characters.get(position) {
429 position += 1;
430 output.push(nested);
431 if nested == delimiter && characters.get(position) == Some(&']') {
432 output.push(']');
433 position += 1;
434 break;
435 }
436 }
437 bracket_can_close = true;
438 continue;
439 }
440 if character == '[' {
441 output.push_str("\\[");
442 bracket_can_close = true;
443 continue;
444 }
445 output.push(character);
446 if character == ']' && bracket_can_close {
447 in_bracket = false;
448 } else if character != '^' || bracket_can_close {
449 bracket_can_close = true;
450 }
451 }
452 output
453}
454
455fn expand_postgres_regex(pattern: &str) -> String {
456 let mut output = String::with_capacity(pattern.len());
457 let mut characters = pattern.chars().peekable();
458 let mut in_bracket = false;
459 let mut bracket_can_close = false;
460 while let Some(character) = characters.next() {
461 if character == '\\' {
462 output.push(character);
463 if let Some(escaped) = characters.next() {
464 output.push(escaped);
465 if in_bracket {
466 bracket_can_close = true;
467 }
468 }
469 continue;
470 }
471 if in_bracket {
472 if character == '[' {
473 if let Some(delimiter @ ('.' | ':' | '=')) = characters.peek().copied() {
474 output.push(character);
475 output.push(delimiter);
476 characters.next();
477 while let Some(nested) = characters.next() {
478 output.push(nested);
479 if nested == delimiter && characters.peek() == Some(&']') {
480 output.push(']');
481 characters.next();
482 break;
483 }
484 }
485 bracket_can_close = true;
486 continue;
487 }
488 }
489 output.push(character);
490 if character == ']' && bracket_can_close {
491 in_bracket = false;
492 } else if character != '^' || bracket_can_close {
493 bracket_can_close = true;
494 }
495 continue;
496 }
497 match character {
498 '[' => {
499 in_bracket = true;
500 bracket_can_close = false;
501 output.push(character);
502 }
503 '#' => {
504 for comment in characters.by_ref() {
505 if comment == '\n' {
506 break;
507 }
508 }
509 }
510 whitespace if postgres_expanded_regex_whitespace(whitespace) => {}
511 other => output.push(other),
512 }
513 }
514 output
515}
516
517fn postgres_expanded_regex_whitespace(character: char) -> bool {
518 matches!(
519 character,
520 '\u{0009}'..='\u{000D}'
521 | '\u{0020}'
522 | '\u{1680}'
523 | '\u{2000}'..='\u{2006}'
524 | '\u{2008}'..='\u{200A}'
525 | '\u{2028}'..='\u{2029}'
526 | '\u{205F}'
527 | '\u{3000}'
528 )
529}
530
531fn postgres_basic_regex(pattern: &str) -> String {
532 let mut output = String::with_capacity(pattern.len());
533 let characters = pattern.chars().collect::<Vec<_>>();
534 let mut position = 0usize;
535 let mut in_bracket = false;
536 let mut bracket_can_close = false;
537 let mut at_subexpression_start = true;
538 while let Some(&character) = characters.get(position) {
539 position += 1;
540 if in_bracket {
541 if character == '\\' {
542 output.push_str(r"\\");
543 bracket_can_close = true;
544 continue;
545 }
546 output.push(character);
547 if character == ']' && bracket_can_close {
548 in_bracket = false;
549 at_subexpression_start = false;
550 } else if character != '^' || bracket_can_close {
551 bracket_can_close = true;
552 }
553 continue;
554 }
555 if character == '\\' {
556 match characters.get(position).copied() {
557 Some('(') => {
558 position += 1;
559 output.push('(');
560 at_subexpression_start = true;
561 }
562 Some(')') => {
563 position += 1;
564 output.push(')');
565 at_subexpression_start = false;
566 }
567 Some(bound @ ('{' | '}')) => {
568 position += 1;
569 output.push(bound);
570 }
571 Some(escaped) if escaped.is_ascii_alphabetic() => {
572 position += 1;
573 output.push(escaped);
574 at_subexpression_start = false;
575 }
576 Some(escaped) => {
577 position += 1;
578 output.push('\\');
579 output.push(escaped);
580 at_subexpression_start = false;
581 }
582 None => output.push('\\'),
583 }
584 continue;
585 }
586 match character {
587 '[' => {
588 in_bracket = true;
589 bracket_can_close = false;
590 output.push(character);
591 }
592 '^' if at_subexpression_start => output.push(character),
593 '^' => {
594 output.push_str(r"\^");
595 at_subexpression_start = false;
596 }
597 '$' => {
598 let closes_subexpression = matches!(
599 (characters.get(position), characters.get(position + 1)),
600 (Some('\\'), Some(')'))
601 );
602 if position == characters.len() || closes_subexpression {
603 output.push(character);
604 } else {
605 output.push_str(r"\$");
606 at_subexpression_start = false;
607 }
608 }
609 '*' if at_subexpression_start => {
610 output.push_str(r"\*");
611 at_subexpression_start = false;
612 }
613 literal @ ('+' | '?' | '(' | ')' | '{' | '}' | '|') => {
614 output.push('\\');
615 output.push(literal);
616 at_subexpression_start = false;
617 }
618 other => {
619 output.push(other);
620 at_subexpression_start = false;
621 }
622 }
623 }
624 output
625}
626
627#[expect(
630 clippy::too_many_lines,
631 reason = "builtin dispatch preserves arity, NULL, and error precedence"
632)]
633pub(super) fn is_quoted_keyword(word: &str) -> bool {
634 const KEYWORDS: &[&str] = &[
635 "all",
636 "analyse",
637 "analyze",
638 "and",
639 "any",
640 "array",
641 "as",
642 "asc",
643 "asymmetric",
644 "authorization",
645 "between",
646 "bigint",
647 "binary",
648 "bit",
649 "boolean",
650 "both",
651 "case",
652 "cast",
653 "char",
654 "character",
655 "check",
656 "coalesce",
657 "collate",
658 "collation",
659 "column",
660 "concurrently",
661 "constraint",
662 "create",
663 "cross",
664 "current_catalog",
665 "current_date",
666 "current_role",
667 "current_schema",
668 "current_time",
669 "current_timestamp",
670 "current_user",
671 "dec",
672 "decimal",
673 "default",
674 "deferrable",
675 "desc",
676 "distinct",
677 "do",
678 "else",
679 "end",
680 "except",
681 "exists",
682 "extract",
683 "false",
684 "fetch",
685 "float",
686 "for",
687 "foreign",
688 "freeze",
689 "from",
690 "full",
691 "grant",
692 "greatest",
693 "group",
694 "grouping",
695 "having",
696 "ilike",
697 "in",
698 "initially",
699 "inner",
700 "inout",
701 "int",
702 "integer",
703 "intersect",
704 "interval",
705 "into",
706 "is",
707 "isnull",
708 "join",
709 "json",
710 "json_array",
711 "json_arrayagg",
712 "json_exists",
713 "json_object",
714 "json_objectagg",
715 "json_query",
716 "json_scalar",
717 "json_serialize",
718 "json_table",
719 "json_value",
720 "lateral",
721 "leading",
722 "least",
723 "left",
724 "like",
725 "limit",
726 "localtime",
727 "localtimestamp",
728 "merge_action",
729 "national",
730 "natural",
731 "nchar",
732 "none",
733 "normalize",
734 "not",
735 "notnull",
736 "null",
737 "nullif",
738 "numeric",
739 "offset",
740 "on",
741 "only",
742 "or",
743 "order",
744 "out",
745 "outer",
746 "overlaps",
747 "overlay",
748 "placing",
749 "position",
750 "precision",
751 "primary",
752 "real",
753 "references",
754 "returning",
755 "right",
756 "row",
757 "select",
758 "session_user",
759 "setof",
760 "similar",
761 "smallint",
762 "some",
763 "substring",
764 "symmetric",
765 "system_user",
766 "table",
767 "tablesample",
768 "then",
769 "time",
770 "timestamp",
771 "to",
772 "trailing",
773 "treat",
774 "trim",
775 "true",
776 "union",
777 "unique",
778 "user",
779 "using",
780 "values",
781 "varchar",
782 "variadic",
783 "verbose",
784 "when",
785 "where",
786 "window",
787 "with",
788 "xmlattributes",
789 "xmlconcat",
790 "xmlelement",
791 "xmlexists",
792 "xmlforest",
793 "xmlnamespaces",
794 "xmlparse",
795 "xmlpi",
796 "xmlroot",
797 "xmlserialize",
798 "xmltable",
799 ];
800 KEYWORDS.binary_search(&word).is_ok()
801}
802
803pub fn quote_ident(ident: &str) -> String {
806 let safe = !ident.is_empty()
807 && ident.chars().enumerate().all(|(i, c)| {
808 c.is_ascii_lowercase() || c == '_' || (i > 0 && (c.is_ascii_digit() || c == '$'))
809 });
810 if safe && !is_quoted_keyword(ident) {
811 return ident.to_string();
812 }
813 format!("\"{}\"", ident.replace('"', "\"\""))
814}
815
816pub(super) fn quote_literal(text: &str) -> String {
819 let escaped = text.replace('\'', "''");
820 if escaped.contains('\\') {
821 format!("E'{}'", escaped.replace('\\', "\\\\"))
822 } else {
823 format!("'{escaped}'")
824 }
825}
826
827pub(super) fn similar_to_regex(pattern: &str, escape: Option<&str>) -> Result<String> {
830 let escape = like_escape_character(escape)?;
831 let mut out = String::with_capacity(pattern.len() + 8);
832 out.push_str("^(?:");
833 let mut after_escape = false;
834 let mut quote_count = 0;
835 let mut bracket_depth = 0usize;
836 let mut bracket_position = 0usize;
837 for character in pattern.chars() {
838 if after_escape {
839 if character == '"' && bracket_depth == 0 {
840 match quote_count {
841 0 => out.push_str("){1,1}?("),
842 1 => out.push_str("){1,1}(?:"),
843 _ => {
844 return Err(SQLError::Routine {
845 sqlstate: "2200C".into(),
846 message: "SQL regular expression may not contain more than two escape-double-quote separators".into(),
847 });
848 }
849 }
850 quote_count += 1;
851 } else {
852 push_similar_escaped(&mut out, character);
853 bracket_position = 3;
854 }
855 after_escape = false;
856 continue;
857 }
858 if escape == Some(character) {
859 after_escape = true;
860 continue;
861 }
862 if bracket_depth > 0 {
863 if character == '\\' && escape != Some('\\') {
864 out.push('\\');
865 }
866 out.push(character);
867 if character == ']' && bracket_position > 2 {
868 bracket_depth -= 1;
869 } else if character == '[' {
870 bracket_depth += 1;
871 bracket_position = 3;
872 } else if character == '^' {
873 bracket_position += 1;
874 } else {
875 bracket_position = 3;
876 }
877 continue;
878 }
879 match character {
880 '%' => out.push_str(".*"),
881 '_' => out.push('.'),
882 '[' => {
883 bracket_depth = 1;
884 bracket_position = 1;
885 out.push('[');
886 }
887 '(' => out.push_str("(?:"),
888 '\\' | '.' | '^' | '$' => {
889 out.push('\\');
890 out.push(character);
891 }
892 other => out.push(other),
893 }
894 }
895 out.push_str(")$");
896 Ok(out)
897}
898
899fn push_similar_escaped(output: &mut String, character: char) {
900 match character {
901 'b' => {
902 output.push_str(r"\x08");
903 return;
904 }
905 'B' => {
906 output.push_str(r"\\");
907 return;
908 }
909 _ => {}
910 }
911 if character.is_ascii_alphanumeric()
912 || matches!(
913 character,
914 '\\' | '.'
915 | '^'
916 | '$'
917 | '|'
918 | '?'
919 | '*'
920 | '+'
921 | '('
922 | ')'
923 | '{'
924 | '}'
925 | '['
926 | ']'
927 | '-'
928 )
929 {
930 output.push('\\');
931 }
932 output.push(character);
933}
934
935#[cfg(test)]
936mod regex_tests;