From abf8402ebfab11b8ed4a39ddb5dc203460bbc9ff Mon Sep 17 00:00:00 2001 From: Brian Hendriks Date: Wed, 15 Mar 2023 16:27:48 -0700 Subject: [PATCH 1/3] support for slashes in database names via url escape codes --- dsn.go | 9 ++++-- dsn_test.go | 81 +++++++++++++++++++++++++++++------------------------ 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/dsn.go b/dsn.go index 4b71aaab..f18e1c49 100644 --- a/dsn.go +++ b/dsn.go @@ -196,7 +196,8 @@ func (cfg *Config) FormatDSN() string { // /dbname buf.WriteByte('/') - buf.WriteString(cfg.DBName) + dbNameEncoded := url.QueryEscape(cfg.DBName) + buf.WriteString(dbNameEncoded) // [?param1=value1&...¶mN=valueN] hasParam := false @@ -358,7 +359,11 @@ func ParseDSN(dsn string) (cfg *Config, err error) { break } } - cfg.DBName = dsn[i+1 : j] + + dbName := dsn[i+1 : j] + if cfg.DBName, err = url.QueryUnescape(dbName); err != nil { + return nil, fmt.Errorf("invalid dbname '%s': %w", dbName, err) + } break } diff --git a/dsn_test.go b/dsn_test.go index 41a6a29f..9d0c2245 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -50,6 +50,9 @@ var testDSNs = []struct { }, { "/dbname", &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, +}, { + "/dbname%2Fwithslash", + &Config{Net: "tcp", Addr: "127.0.0.1:3306", DBName: "dbname/withslash", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, }, { "@/", &Config{Net: "tcp", Addr: "127.0.0.1:3306", Collation: "utf8mb4_general_ci", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, AllowNativePasswords: true, CheckConnLiveness: true}, @@ -76,30 +79,32 @@ var testDSNs = []struct { func TestDSNParser(t *testing.T) { for i, tst := range testDSNs { - cfg, err := ParseDSN(tst.in) - if err != nil { - t.Error(err.Error()) - } + t.Run(tst.in, func(t *testing.T) { + cfg, err := ParseDSN(tst.in) + if err != nil { + t.Error(err.Error()) + return + } - // pointer not static - cfg.TLS = nil + // pointer not static + cfg.TLS = nil - if !reflect.DeepEqual(cfg, tst.out) { - t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) - } + if !reflect.DeepEqual(cfg, tst.out) { + t.Errorf("%d. ParseDSN(%q) mismatch:\ngot %+v\nwant %+v", i, tst.in, cfg, tst.out) + } + }) } } func TestDSNParserInvalid(t *testing.T) { var invalidDSNs = []string{ - "@net(addr/", // no closing brace - "@tcp(/", // no closing brace - "tcp(/", // no closing brace - "(/", // no closing brace - "net(addr)//", // unescaped - "User:pass@tcp(1.2.3.4:3306)", // no trailing slash - "net()/", // unknown default addr - "user:pass@tcp(127.0.0.1:3306)/db/name", // invalid dbname + "@net(addr/", // no closing brace + "@tcp(/", // no closing brace + "tcp(/", // no closing brace + "(/", // no closing brace + "net(addr)//", // unescaped + "User:pass@tcp(1.2.3.4:3306)", // no trailing slash + "net()/", // unknown default addr "user:password@/dbname?allowFallbackToPlaintext=PREFERRED", // wrong bool flag //"/dbname?arg=/some/unescaped/path", } @@ -113,27 +118,29 @@ func TestDSNParserInvalid(t *testing.T) { func TestDSNReformat(t *testing.T) { for i, tst := range testDSNs { - dsn1 := tst.in - cfg1, err := ParseDSN(dsn1) - if err != nil { - t.Error(err.Error()) - continue - } - cfg1.TLS = nil // pointer not static - res1 := fmt.Sprintf("%+v", cfg1) - - dsn2 := cfg1.FormatDSN() - cfg2, err := ParseDSN(dsn2) - if err != nil { - t.Error(err.Error()) - continue - } - cfg2.TLS = nil // pointer not static - res2 := fmt.Sprintf("%+v", cfg2) + t.Run(tst.in, func(t *testing.T) { + dsn1 := tst.in + cfg1, err := ParseDSN(dsn1) + if err != nil { + t.Error(err.Error()) + return + } + cfg1.TLS = nil // pointer not static + res1 := fmt.Sprintf("%+v", cfg1) - if res1 != res2 { - t.Errorf("%d. %q does not match %q", i, res2, res1) - } + dsn2 := cfg1.FormatDSN() + cfg2, err := ParseDSN(dsn2) + if err != nil { + t.Error(err.Error()) + return + } + cfg2.TLS = nil // pointer not static + res2 := fmt.Sprintf("%+v", cfg2) + + if res1 != res2 { + t.Errorf("%d. %q does not match %q", i, res2, res1) + } + }) } } From c72e660bb1dec3019ff82dae884d7a6844285305 Mon Sep 17 00:00:00 2001 From: Brian Hendriks Date: Wed, 15 Mar 2023 17:06:44 -0700 Subject: [PATCH 2/3] revert test change --- dsn_test.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/dsn_test.go b/dsn_test.go index 9d0c2245..83ea8caa 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -98,13 +98,14 @@ func TestDSNParser(t *testing.T) { func TestDSNParserInvalid(t *testing.T) { var invalidDSNs = []string{ - "@net(addr/", // no closing brace - "@tcp(/", // no closing brace - "tcp(/", // no closing brace - "(/", // no closing brace - "net(addr)//", // unescaped - "User:pass@tcp(1.2.3.4:3306)", // no trailing slash - "net()/", // unknown default addr + "@net(addr/", // no closing brace + "@tcp(/", // no closing brace + "tcp(/", // no closing brace + "(/", // no closing brace + "net(addr)//", // unescaped + "User:pass@tcp(1.2.3.4:3306)", // no trailing slash + "net()/", // unknown default addr + "user:pass@tcp(127.0.0.1:3306)/db/name", // invalid dbname "user:password@/dbname?allowFallbackToPlaintext=PREFERRED", // wrong bool flag //"/dbname?arg=/some/unescaped/path", } From afee07310373caca75aa679c362345ab7d55af69 Mon Sep 17 00:00:00 2001 From: Brian Hendriks Date: Thu, 16 Mar 2023 11:07:55 -0700 Subject: [PATCH 3/3] Update AUTHORS file and README.md --- AUTHORS | 2 ++ README.md | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/AUTHORS b/AUTHORS index 6f7041c7..09610525 100644 --- a/AUTHORS +++ b/AUTHORS @@ -106,6 +106,7 @@ Xuehong Chan Zhenye Xie Zhixin Wen Ziheng Lyu +Brian Hendriks # Organizations @@ -123,3 +124,4 @@ Percona LLC Pivotal Inc. Stripe Inc. Zendesk Inc. +Dolthub Inc. diff --git a/README.md b/README.md index 25de2e5a..49c71802 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,11 @@ This has the same effect as an empty DSN string: ``` +If your database name includes a slash, use the [URL encoding](https://en.wikipedia.org/wiki/Percent-encoding) `%2F`: +``` +/dbname%2Fwithslash +``` + Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct. #### Password