From 28db6f5206361c7e5f28a198a5c81fffabf8732a Mon Sep 17 00:00:00 2001 From: Sravan Sridhar Date: Fri, 29 May 2026 18:52:17 +0530 Subject: [PATCH] fix: escape regex metacharacters in Q.like on the LokiJS adapter `likeToRegexp` (LokiJS/web adapter, used for Q.like/Q.notLike) built a RegExp from the LIKE pattern without escaping regex special characters. SQL LIKE treats everything except `%`/`_` as a literal, and the SQLite adapter does too, so the two adapters diverged: - silent wrong results: `Q.like('%.pdf')` matched 'docXpdf' (`.` = any char) on LokiJS but only real '.pdf' on SQLite; `Q.like('a.b')` matched 'axb' - crash: `Q.like('a(b')` / `Q.like('%(foo)%')` threw "Invalid regular expression" Escape regex metacharacters before substituting the `%`/`_` wildcards. The `s`/`i` flags are unchanged, so dotall + case-insensitive behaviour is preserved. Adds regression tests. Co-Authored-By: Claude Opus 4.8 --- src/utils/fp/likeToRegexp/index.js | 6 +++++- src/utils/fp/likeToRegexp/test.js | 33 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/utils/fp/likeToRegexp/test.js diff --git a/src/utils/fp/likeToRegexp/index.js b/src/utils/fp/likeToRegexp/index.js index c7d2b2b2b..0a648d2d1 100644 --- a/src/utils/fp/likeToRegexp/index.js +++ b/src/utils/fp/likeToRegexp/index.js @@ -1,6 +1,10 @@ // @flow export default function likeToRegexp(likeQuery: string): RegExp { - const regexp = `^${likeQuery}$`.replace(/%/g, '.*').replace(/_/g, '.') + // Escape regex special characters so the pattern is matched literally — the + // same way the SQLite adapter treats a LIKE pattern. `%` and `_` are + // intentionally not escaped here so they remain LIKE wildcards below. + const escaped = likeQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const regexp = `^${escaped}$`.replace(/%/g, '.*').replace(/_/g, '.') return new RegExp(regexp, 'is') } diff --git a/src/utils/fp/likeToRegexp/test.js b/src/utils/fp/likeToRegexp/test.js new file mode 100644 index 000000000..7af52a9cc --- /dev/null +++ b/src/utils/fp/likeToRegexp/test.js @@ -0,0 +1,33 @@ +import likeToRegexp from './index' + +describe('likeToRegexp', () => { + it('treats % and _ as wildcards', () => { + expect(likeToRegexp('100%').test('100abc')).toBe(true) + expect(likeToRegexp('a_b').test('axb')).toBe(true) + expect(likeToRegexp('a_b').test('ab')).toBe(false) + }) + + it('matches newlines (dotall) and is case-insensitive', () => { + expect(likeToRegexp('a%b').test('a\nb')).toBe(true) + expect(likeToRegexp('a_b').test('a\nb')).toBe(true) + expect(likeToRegexp('ABC').test('abc')).toBe(true) + }) + + it('treats regex special characters as literals (matching the SQLite adapter)', () => { + // `.` is a literal dot, not "any character" + expect(likeToRegexp('a.b').test('a.b')).toBe(true) + expect(likeToRegexp('a.b').test('axb')).toBe(false) + expect(likeToRegexp('%.pdf').test('doc.pdf')).toBe(true) + expect(likeToRegexp('%.pdf').test('docXpdf')).toBe(false) + // `+` is a literal, not a quantifier + expect(likeToRegexp('c+').test('c+')).toBe(true) + expect(likeToRegexp('c+').test('cccc')).toBe(false) + }) + + it('does not crash on unbalanced regex metacharacters', () => { + expect(() => likeToRegexp('a(b')).not.toThrow() + expect(likeToRegexp('a(b').test('a(b')).toBe(true) + expect(() => likeToRegexp('a[b')).not.toThrow() + expect(likeToRegexp('a[b').test('a[b')).toBe(true) + }) +})