Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions cypher/models/cypher/format/format_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
})
}
}
6 changes: 5 additions & 1 deletion cypher/models/cypher/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading