From 653c2fcf251e6b5a9444596cf17af7ffda83dd0a Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:06:54 -0700 Subject: [PATCH] escape special characters in string literals --- cypher/models/cypher/format/format_test.go | 123 +++++++++++++++++++++ cypher/models/cypher/model.go | 6 +- 2 files changed, 128 insertions(+), 1 deletion(-) diff --git a/cypher/models/cypher/format/format_test.go b/cypher/models/cypher/format/format_test.go index 5b431d2..136238a 100644 --- a/cypher/models/cypher/format/format_test.go +++ b/cypher/models/cypher/format/format_test.go @@ -4,6 +4,7 @@ import ( "bytes" "testing" + "github.com/specterops/dawgs/cypher/models/cypher" "github.com/specterops/dawgs/cypher/models/cypher/format" "github.com/specterops/dawgs/cypher/frontend" @@ -34,3 +35,125 @@ func TestCypherEmitter_HappyPath(t *testing.T) { func TestCypherEmitter_NegativeCases(t *testing.T) { test.LoadFixture(t, test.NegativeTestCases).Run(t) } + +func TestNewStringLiteral_Escaping(t *testing.T) { + testCases := []struct { + name string + input string + expected string + }{ + { + name: "backslash should be escaped", + input: `MAYYHEM\PS1-PSV$@`, + expected: `'MAYYHEM\\PS1-PSV$@'`, + }, + { + name: "single quote should be escaped", + input: `O'Brien`, + expected: `'O\'Brien'`, + }, + { + name: "both backslash and single quote", + input: `path\to\file's location`, + expected: `'path\\to\\file\'s location'`, + }, + { + name: "multiple backslashes", + input: `C:\Windows\System32`, + expected: `'C:\\Windows\\System32'`, + }, + { + name: "no special characters", + input: `simple_value`, + expected: `'simple_value'`, + }, + { + name: "empty string", + input: ``, + expected: `''`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + literal := cypher.NewStringLiteral(tc.input) + require.NotNil(t, literal) + require.Equal(t, tc.expected, literal.Value) + }) + } +} + +func TestNewStringLiteral_InQuery(t *testing.T) { + // Test that escaped string literals work correctly in actual Cypher queries + testCases := []struct { + name string + propertyKey string + value string + expectedQuery string + }{ + { + name: "backslash in objectid", + propertyKey: "objectid", + value: `MAYYHEM\PS1-PSV$@`, + expectedQuery: `match (n {objectid: 'MAYYHEM\\PS1-PSV$@'}) return n`, + }, + { + name: "single quote in name", + propertyKey: "name", + value: `O'Brien`, + expectedQuery: `match (n {name: 'O\'Brien'}) return n`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Build a query using NewStringLiteral + literal := cypher.NewStringLiteral(tc.value) + + // Create a simple query structure + query := &cypher.RegularQuery{ + SingleQuery: &cypher.SingleQuery{ + SinglePartQuery: &cypher.SinglePartQuery{ + ReadingClauses: []*cypher.ReadingClause{ + { + Match: &cypher.Match{ + Pattern: []*cypher.PatternPart{ + { + PatternElements: []*cypher.PatternElement{ + { + Element: &cypher.NodePattern{ + Variable: &cypher.Variable{Symbol: "n"}, + Properties: cypher.MapLiteral{ + tc.propertyKey: literal, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Return: &cypher.Return{ + Projection: &cypher.Projection{ + Items: []cypher.Expression{ + &cypher.ProjectionItem{ + Expression: &cypher.Variable{Symbol: "n"}, + }, + }, + }, + }, + }, + }, + } + + // Format the query + buffer := &bytes.Buffer{} + emitter := format.NewCypherEmitter(false) + err := emitter.Write(query, buffer) + + require.Nil(t, err) + require.Equal(t, tc.expectedQuery, buffer.String()) + }) + } +} diff --git a/cypher/models/cypher/model.go b/cypher/models/cypher/model.go index aff1f0f..c36a619 100644 --- a/cypher/models/cypher/model.go +++ b/cypher/models/cypher/model.go @@ -783,7 +783,11 @@ func NewLiteral(value any, null bool) *Literal { } func NewStringLiteral(value string) *Literal { - return NewLiteral("'"+value+"'", false) + // Escape backslashes and single quotes for Cypher string literals + // In Cypher: backslash must be \\ and single quote must be \' + escaped := strings.ReplaceAll(value, "\\", "\\\\") + escaped = strings.ReplaceAll(escaped, "'", "\\'") + return NewLiteral("'"+escaped+"'", false) } func (s *Literal) copy() *Literal {