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