diff --git a/core/Cargo.toml b/core/Cargo.toml index 4d731eb2b..1f2285f5f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -44,7 +44,8 @@ sieve-cache = "0.1.4" sqlite3-parser = { path = "../vendored/sqlite3-parser" } thiserror = "1.0.61" getrandom = { version = "0.2.15", features = ["js"] } -regex = "1.10.5" +regex = "1.11.1" +regex-syntax = { version = "0.8.5", default-features = false, features = ["unicode"] } chrono = "0.4.38" julian_day_converter = "0.3.2" jsonb = { version = "0.4.4", optional = true } diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 92ef84028..980aab282 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -46,7 +46,7 @@ use datetime::{exec_date, exec_time, exec_unixepoch}; use rand::distributions::{Distribution, Uniform}; use rand::{thread_rng, Rng}; -use regex::Regex; +use regex::{Regex, RegexBuilder}; use std::borrow::{Borrow, BorrowMut}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; @@ -3199,10 +3199,31 @@ fn exec_char(values: Vec) -> OwnedValue { } fn construct_like_regex(pattern: &str) -> Regex { - let mut regex_pattern = String::from("(?i)^"); - regex_pattern.push_str(&pattern.replace('%', ".*").replace('_', ".")); + let mut regex_pattern = String::with_capacity(pattern.len() * 2); + + regex_pattern.push('^'); + + for c in pattern.chars() { + match c { + '\\' => regex_pattern.push_str("\\\\"), + '%' => regex_pattern.push_str(".*"), + '_' => regex_pattern.push('.'), + ch => { + if regex_syntax::is_meta_character(c) { + regex_pattern.push('\\'); + } + regex_pattern.push(ch); + } + } + } + regex_pattern.push('$'); - Regex::new(®ex_pattern).unwrap() + + RegexBuilder::new(®ex_pattern) + .case_insensitive(true) + .dot_matches_new_line(true) + .build() + .unwrap() } // Implements LIKE pattern matching. Caches the constructed regex if a cache is provided @@ -4353,12 +4374,18 @@ mod tests { ); } + #[test] + fn test_like_with_escape_or_regexmeta_chars() { + assert!(exec_like(None, r#"\%A"#, r#"\A"#)); + assert!(exec_like(None, "%a%a", "aaaa")); + } + #[test] fn test_like_no_cache() { assert!(exec_like(None, "a%", "aaaa")); assert!(exec_like(None, "%a%a", "aaaa")); - assert!(exec_like(None, "%a.a", "aaaa")); - assert!(exec_like(None, "a.a%", "aaaa")); + assert!(!exec_like(None, "%a.a", "aaaa")); + assert!(!exec_like(None, "a.a%", "aaaa")); assert!(!exec_like(None, "%a.ab", "aaaa")); } @@ -4367,15 +4394,15 @@ mod tests { let mut cache = HashMap::new(); assert!(exec_like(Some(&mut cache), "a%", "aaaa")); assert!(exec_like(Some(&mut cache), "%a%a", "aaaa")); - assert!(exec_like(Some(&mut cache), "%a.a", "aaaa")); - assert!(exec_like(Some(&mut cache), "a.a%", "aaaa")); + assert!(!exec_like(Some(&mut cache), "%a.a", "aaaa")); + assert!(!exec_like(Some(&mut cache), "a.a%", "aaaa")); assert!(!exec_like(Some(&mut cache), "%a.ab", "aaaa")); // again after values have been cached assert!(exec_like(Some(&mut cache), "a%", "aaaa")); assert!(exec_like(Some(&mut cache), "%a%a", "aaaa")); - assert!(exec_like(Some(&mut cache), "%a.a", "aaaa")); - assert!(exec_like(Some(&mut cache), "a.a%", "aaaa")); + assert!(!exec_like(Some(&mut cache), "%a.a", "aaaa")); + assert!(!exec_like(Some(&mut cache), "a.a%", "aaaa")); assert!(!exec_like(Some(&mut cache), "%a.ab", "aaaa")); } diff --git a/testing/like.test b/testing/like.test index edd6ba5e5..a52b90b60 100755 --- a/testing/like.test +++ b/testing/like.test @@ -77,3 +77,15 @@ Robert|Roberts} do_execsql_test where-like-impossible { select * from products where 'foobar' like 'fooba'; } {} + +do_execsql_test like-with-backslash { + select like('\%A', '\A') +} {1} + +do_execsql_test like-with-dollar { + select like('A$%', 'A$') +} {1} + +do_execsql_test like-with-dot { + select like('%a.a', 'aaaa') +} {0}