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) + }) +})