From 91e2085c424e6ff0c8745dae70f34e8e57834b70 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Thu, 7 May 2026 01:20:12 -0700 Subject: [PATCH] python driver: preserve trailing quotes in agtype string values str.strip('"') in visitStringValue() and visitPair() removes every '"' on either side of the token, not just the outer delimiters, so a value ending in an escaped quote (e.g. '"foo \"bar\""') loses its trailing backslash-escaped '"' character. The Agtype grammar guarantees STRING tokens always carry exactly one delimiter on each side, so slice with [1:-1] to strip them precisely. Fixes #2418 Signed-off-by: SAY-5 --- drivers/python/age/builder.py | 10 ++++++++-- drivers/python/test_agtypes.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/drivers/python/age/builder.py b/drivers/python/age/builder.py index 08a40c252..a93e681ce 100644 --- a/drivers/python/age/builder.py +++ b/drivers/python/age/builder.py @@ -107,7 +107,12 @@ def visitAgValue(self, ctx:AgtypeParser.AgValueContext): # Visit a parse tree produced by AgtypeParser#StringValue. def visitStringValue(self, ctx:AgtypeParser.StringValueContext): - return ctx.STRING().getText().strip('"') + # The STRING token always has surrounding '"' delimiters per the + # Agtype grammar. str.strip('"') would also drop any '"' characters + # that are part of the actual data when the value starts or ends + # with an escaped quote, e.g. '"foo \\"bar\\""' -> 'foo \\"bar\\', + # so trim exactly the first and last character instead. + return ctx.STRING().getText()[1:-1] # Visit a parse tree produced by AgtypeParser#IntegerValue. @@ -182,7 +187,8 @@ def visitPair(self, ctx:AgtypeParser.PairContext): raise AGTypeError(ctx.getText(), "Missing key in object pair") if agValNode is None: raise AGTypeError(ctx.getText(), "Missing value in object pair") - return (strNode.getText().strip('"') , agValNode) + # See visitStringValue() for why we slice instead of using strip('"'). + return (strNode.getText()[1:-1] , agValNode) # Visit a parse tree produced by AgtypeParser#array. diff --git a/drivers/python/test_agtypes.py b/drivers/python/test_agtypes.py index 97f7972d1..62858a9cb 100644 --- a/drivers/python/test_agtypes.py +++ b/drivers/python/test_agtypes.py @@ -245,6 +245,20 @@ def test_array_of_mixed_types(self): self.assertEqual(result[4], [1, 2, 3]) self.assertEqual(result[5], {"key": "val"}) + def test_string_value_preserves_inner_quotes(self): + """Issue #2418: visitStringValue must remove only the outer quote + delimiters, not every '"' on either side, otherwise values that end + with an escaped quote (e.g. '"foo \\"bar\\""') lose data.""" + self.assertEqual(self.parse('"foo \\"bar\\""'), 'foo \\"bar\\"') + self.assertEqual(self.parse('"\\"leading"'), '\\"leading') + self.assertEqual(self.parse('"trailing\\""'), 'trailing\\"') + self.assertEqual(self.parse('""'), '') + # Same fix applies to visitPair() for object keys. + self.assertEqual( + self.parse('{"key\\"q": 1}'), + {'key\\"q': 1}, + ) + def test_malformed_vertex_raises_agtypeerror_or_recovers(self): """Issue #2367: Malformed agtype must raise AGTypeError or recover gracefully.""" from age.exceptions import AGTypeError