From d652f91da7fd118cded9e0210275430ba43bc6cd Mon Sep 17 00:00:00 2001 From: Denis Rouzaud Date: Wed, 30 Sep 2026 09:18:41 +0200 Subject: [PATCH] fix(cql2-text): unescape the quotes of strings --- pygeofilter/parsers/cql2_text/grammar.lark | 3 ++- pygeofilter/parsers/cql2_text/parser.py | 9 ++++++- tests/parsers/cql2_text/test_parser.py | 30 ++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pygeofilter/parsers/cql2_text/grammar.lark b/pygeofilter/parsers/cql2_text/grammar.lark index 703cb43..1481e2e 100644 --- a/pygeofilter/parsers/cql2_text/grammar.lark +++ b/pygeofilter/parsers/cql2_text/grammar.lark @@ -135,7 +135,8 @@ bbox: "BBOX"i "(" full_number "," full_number "," full_number "," full_number ") BOOLEAN.2: ( "TRUE"i | "FALSE"i) DOUBLE_QUOTED: "\"" /.*?/ "\"" -SINGLE_QUOTED: "'" /.*?/ "'" +// CQL2 escapes a quote as '' or \' +SINGLE_QUOTED: /'(?:[^'\\]|\\.|'')*'/ DATE: /[0-9]{4}-?[0-1][0-9]-?[0-3][0-9]/ DATETIME: /[0-9]{4}-?[0-1][0-9]-?[0-3][0-9][T ][0-2][0-9]:?[0-5][0-9]:?[0-5][0-9](\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})?/ diff --git a/pygeofilter/parsers/cql2_text/parser.py b/pygeofilter/parsers/cql2_text/parser.py index cced284..768ee8d 100644 --- a/pygeofilter/parsers/cql2_text/parser.py +++ b/pygeofilter/parsers/cql2_text/parser.py @@ -27,6 +27,7 @@ import logging import os.path +import re from lark import Lark, logger, v_args @@ -185,7 +186,13 @@ def DOUBLE_QUOTED(self, token): return token[1:-1] def SINGLE_QUOTED(self, token): - return token[1:-1] + # '' and \' stand for a quote, the other backslashes are kept for LIKE + return re.sub( + r"''|\\(.)", + lambda m: "'" if m[0] == "''" or m[1] == "'" else m[0], + token[1:-1], + flags=re.S, + ) def geometry(self, value): return values.Geometry(value) diff --git a/tests/parsers/cql2_text/test_parser.py b/tests/parsers/cql2_text/test_parser.py index 5bceec7..51e2d41 100644 --- a/tests/parsers/cql2_text/test_parser.py +++ b/tests/parsers/cql2_text/test_parser.py @@ -175,6 +175,36 @@ def test_string_not_like(): ) +def test_string_doubled_quote(): + result = parse("attr = 'it''s'") + assert result == ast.Equal(ast.Attribute("attr"), "it's") + + +def test_string_backslash_quote(): + result = parse(r"attr = 'it\'s'") + assert result == ast.Equal(ast.Attribute("attr"), "it's") + + +def test_string_only_quotes(): + assert parse("attr = ''") == ast.Equal(ast.Attribute("attr"), "") + assert parse("attr = ''''") == ast.Equal(ast.Attribute("attr"), "'") + + +def test_strings_end_at_their_quote(): + result = parse("attr = 'a' OR attr = 'b'") + assert result == ast.Or( + ast.Equal(ast.Attribute("attr"), "a"), + ast.Equal(ast.Attribute("attr"), "b"), + ) + + +def test_string_like_keeps_backslashes(): + result = parse(r"attr LIKE '100\%'") + assert result.pattern == r"100\%" + result = parse(r"attr LIKE 'a\\''%'") + assert result.pattern == r"a\\'%" + + def test_attribute_in_list(): result = parse("attr IN (1, 2, 3, 4)") assert result == ast.In(