mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-12 01:24:12 +08:00
- isMySQLLikeDBType 更名为 dialectEscapesBackslashInStringLiteral,按语义而非协议族命名 - 新增 clickhouse/tdengine/taos:这三个方言的字符串字面量同样支持 C 风格反斜杠转义, 上一提交漏掉会导致其 dump 还原时仍被静默改写 - 补充双向断言:漏加方言(数据被改写)与误加方言(反斜杠被写成两个)都会测试失败
131 lines
6.6 KiB
Go
131 lines
6.6 KiB
Go
package app
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// TestFormatSQLValueEscapesBackslashForMySQLLikeDialects 覆盖 SQL 导出的反斜杠转义。
|
||
//
|
||
// 回归背景:formatSQLValue 原先只把单引号翻倍、不处理反斜杠。MySQL 系方言默认 sql_mode
|
||
// 不含 NO_BACKSLASH_ESCAPES,导致 (a) 含反斜杠的普通文本(Windows 路径、正则、转义 JSON)
|
||
// 在导出→还原后被静默改写;(b) 以反斜杠结尾的值会让 '...\' 吞掉闭合单引号,使后续生成的
|
||
// SQL 文本越出字面量,源库中的恶意行可在用户还原 dump 时执行任意 SQL。
|
||
func TestFormatSQLValueEscapesBackslashForMySQLLikeDialects(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
dbType string
|
||
value interface{}
|
||
want string
|
||
}{
|
||
{name: "mysql 单个反斜杠翻倍", dbType: "mysql", value: `\`, want: `'\\'`},
|
||
{name: "mysql Windows 路径", dbType: "mysql", value: `C:\temp\new`, want: `'C:\\temp\\new'`},
|
||
{name: "mysql 已转义的 JSON", dbType: "mysql", value: `{"k":"a\nb"}`, want: `'{"k":"a\\nb"}'`},
|
||
{name: "mariadb 同样翻倍", dbType: "mariadb", value: `a\b`, want: `'a\\b'`},
|
||
{name: "tidb 同样翻倍", dbType: "tidb", value: `a\b`, want: `'a\\b'`},
|
||
{name: "oceanbase 同样翻倍", dbType: "oceanbase", value: `a\b`, want: `'a\\b'`},
|
||
{name: "diros 同样翻倍", dbType: "diros", value: `a\b`, want: `'a\\b'`},
|
||
{name: "starrocks 同样翻倍", dbType: "starrocks", value: `a\b`, want: `'a\\b'`},
|
||
|
||
// 非 MySQL 系:standard_conforming_strings 下反斜杠是字面量,翻倍反而会损坏数据。
|
||
{name: "postgres 保持反斜杠原样", dbType: "postgres", value: `C:\temp`, want: `'C:\temp'`},
|
||
{name: "sqlserver 保持反斜杠原样", dbType: "sqlserver", value: `C:\temp`, want: `'C:\temp'`},
|
||
{name: "oracle 保持反斜杠原样", dbType: "oracle", value: `C:\temp`, want: `'C:\temp'`},
|
||
|
||
// 单引号翻倍的既有行为必须保持不变。
|
||
{name: "mysql 单引号仍翻倍", dbType: "mysql", value: `O'Brien`, want: `'O''Brien'`},
|
||
{name: "postgres 单引号仍翻倍", dbType: "postgres", value: `O'Brien`, want: `'O''Brien'`},
|
||
{name: "mysql 反斜杠与单引号并存", dbType: "mysql", value: `a\'b`, want: `'a\\''b'`},
|
||
|
||
// MySQL 十六进制字面量直通的既有旁路必须保持不变。
|
||
{name: "mysql 十六进制字面量直通", dbType: "mysql", value: "0xDEADBEEF", want: "0xDEADBEEF"},
|
||
{name: "postgres 不走十六进制直通", dbType: "postgres", value: "0xDEADBEEF", want: "'0xDEADBEEF'"},
|
||
|
||
{name: "nil 仍为 NULL", dbType: "mysql", value: nil, want: "NULL"},
|
||
}
|
||
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
if got := formatSQLValue(tc.dbType, tc.value); got != tc.want {
|
||
t.Fatalf("formatSQLValue(%q, %#v) = %s,期望 %s", tc.dbType, tc.value, got, tc.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestFormatSQLValueBlocksStatementBreakoutOnRoundTrip 用项目自己的切分器做闭环回归:
|
||
// 导出的 INSERT 在被本仓库的 SQL 切分器读回时,绝不能裂出额外的可执行语句。
|
||
func TestFormatSQLValueBlocksStatementBreakoutOnRoundTrip(t *testing.T) {
|
||
// 攻击载荷:第一列是单个反斜杠,第二列试图闭合字面量并追加 DROP。
|
||
// 未修复前 '\' 会吞掉闭合引号,使 DROP TABLE users 成为独立语句。
|
||
c1 := formatSQLValue("mysql", `\`)
|
||
c2 := formatSQLValue("mysql", `); DROP TABLE users; -- `)
|
||
stmt := "INSERT INTO `t` (`c1`, `c2`) VALUES (" + c1 + ", " + c2 + ");"
|
||
|
||
got := splitSQLStatementsForDialect("mysql", stmt)
|
||
if len(got) != 1 {
|
||
t.Fatalf("导出语句被切分成 %d 条,期望 1 条(字面量越界):%#v", len(got), got)
|
||
}
|
||
// 载荷文本出现在这条 INSERT 的字符串字面量内部是正常的;关键是它没有成为独立语句。
|
||
if !strings.HasPrefix(strings.TrimSpace(strings.ToUpper(got[0])), "INSERT") {
|
||
t.Fatalf("唯一语句不是 INSERT,字面量可能已越界:%q", got[0])
|
||
}
|
||
|
||
// 反向对照:还原修复前的行为(只翻倍单引号、不转义反斜杠),
|
||
// 确认该场景确实会裂出独立的 DROP 语句——以此证明上面的断言真能抓住这个回归。
|
||
unescaped := "INSERT INTO `t` (`c1`, `c2`) VALUES ('" +
|
||
strings.ReplaceAll(`\`, "'", "''") + "', '" +
|
||
strings.ReplaceAll(`); DROP TABLE users; -- `, "'", "''") + "');"
|
||
legacy := splitSQLStatementsForDialect("mysql", unescaped)
|
||
foundStandaloneDrop := false
|
||
for _, s := range legacy {
|
||
if strings.HasPrefix(strings.TrimSpace(strings.ToUpper(s)), "DROP TABLE") {
|
||
foundStandaloneDrop = true
|
||
break
|
||
}
|
||
}
|
||
if !foundStandaloneDrop {
|
||
t.Fatalf("反向对照失效:未转义反斜杠时本应裂出独立 DROP 语句,实际切分为 %#v", legacy)
|
||
}
|
||
}
|
||
|
||
// TestEscapeSQLStringLiteralBodyIsDialectAware 直接覆盖转义辅助函数的方言分派。
|
||
func TestEscapeSQLStringLiteralBodyIsDialectAware(t *testing.T) {
|
||
if got := escapeSQLStringLiteralBody("mysql", `a\b'c`); got != `a\\b''c` {
|
||
t.Fatalf("mysql 转义结果 = %q,期望 %q", got, `a\\b''c`)
|
||
}
|
||
if got := escapeSQLStringLiteralBody("postgres", `a\b'c`); got != `a\b''c` {
|
||
t.Fatalf("postgres 转义结果 = %q,期望 %q", got, `a\b''c`)
|
||
}
|
||
}
|
||
|
||
// TestDialectEscapesBackslashInStringLiteralCoversAffectedDialects 固定"字面量内反斜杠是
|
||
// 转义符"的方言集合。新增方言时若漏加,导出的 dump 会静默改写数据;若误加,反斜杠会被
|
||
// 写成两个而同样损坏数据——两个方向都必须在此失败。
|
||
func TestDialectEscapesBackslashInStringLiteralCoversAffectedDialects(t *testing.T) {
|
||
// MySQL 协议系 + ClickHouse/TDengine 的字面量都支持反斜杠转义。
|
||
for _, dbType := range []string{
|
||
"mysql", "MySQL", " mariadb ", "tidb", "oceanbase", "diros", "doris", "starrocks",
|
||
"clickhouse", "tdengine", "taos",
|
||
} {
|
||
if !dialectEscapesBackslashInStringLiteral(dbType) {
|
||
t.Errorf("dialectEscapesBackslashInStringLiteral(%q) = false,期望 true", dbType)
|
||
}
|
||
}
|
||
// standard_conforming_strings / 无反斜杠转义语义的方言:反斜杠必须原样保留。
|
||
for _, dbType := range []string{"postgres", "postgresql", "sqlserver", "oracle", "sqlite", "kingbase", "highgo", "gaussdb", ""} {
|
||
if dialectEscapesBackslashInStringLiteral(dbType) {
|
||
t.Errorf("dialectEscapesBackslashInStringLiteral(%q) = true,期望 false", dbType)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestFormatSQLValueEscapesBackslashForClickHouseAndTDengine 覆盖初版修复漏掉的方言。
|
||
func TestFormatSQLValueEscapesBackslashForClickHouseAndTDengine(t *testing.T) {
|
||
for _, dbType := range []string{"clickhouse", "tdengine"} {
|
||
if got := formatSQLValue(dbType, `a\b`); got != `'a\\b'` {
|
||
t.Errorf("formatSQLValue(%q, %q) = %s,期望 %s", dbType, `a\b`, got, `'a\\b'`)
|
||
}
|
||
}
|
||
}
|