diff --git a/batch/batch.go b/batch/batch.go index 7c23241e..a8b9c725 100644 --- a/batch/batch.go +++ b/batch/batch.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" "unicode" + "unicode/utf8" ) // Split the provided SQL into multiple sql scripts based on a given @@ -46,6 +47,18 @@ func hasPrefixFold(s, sep string) bool { if len(s) < len(sep) { return false } + + // Reject matches where the separator is followed by another letter, + // so e.g. "GO" does not match the start of "GOTO". Use DecodeRuneInString + // to handle multi-byte runes correctly; a bare rune(s[i]) cast would + // misclassify the leading byte of a multi-byte sequence. + if len(s) > len(sep) { + r, _ := utf8.DecodeRuneInString(s[len(sep):]) + if unicode.IsLetter(r) { + return false + } + } + return strings.EqualFold(s[:len(sep)], sep) } diff --git a/batch/batch_test.go b/batch/batch_test.go index fcafdd3b..bfba9dd6 100644 --- a/batch/batch_test.go +++ b/batch/batch_test.go @@ -67,6 +67,15 @@ select top 1 1`, select top 1 1`, }, }, + testItem{ + Sql: `PRINT 1 +GOTO Bookmark +GO +PRINT 2 +Bookmark: +GO`, + Expect: []string{"PRINT 1\nGOTO Bookmark\n", "\nPRINT 2\nBookmark:\n"}, + }, testItem{ Sql: ` create table t ( @@ -134,6 +143,18 @@ func TestHasPrefixFold(t *testing.T) { {"h", "H", true}, {"h", "K", false}, {"go 5\n", "go", true}, + // Word-boundary checks: separator must not be followed by another letter. + {"GOTO foo", "GO", false}, + {"gotoflag", "go", false}, + {"GO1\n", "GO", true}, + {"GO_FOO\n", "GO", true}, + // Multi-byte UTF-8 follower. Hebrew aleph (U+05D0) is encoded as + // 0xD7 0x90; a bare rune(s[i]) cast would see 0xD7 (× MULTIPLICATION + // SIGN, not a letter) and incorrectly allow the match. Decoding the + // rune correctly sees U+05D0 (a letter) and rejects. + {"GO\u05D0test", "GO", false}, + // Latin-1 letter follower (single-byte path). + {"GOé", "GO", false}, } for _, item := range list { is := hasPrefixFold(item.s, item.pre)