@@ -5,11 +5,13 @@ package sqlxx
55
66import (
77 "fmt"
8+ "net/url"
89 "reflect"
910 "slices"
1011 "strings"
1112
1213 "github.com/jmoiron/sqlx/reflectx"
14+ "github.com/pkg/errors"
1315)
1416
1517// GetDBFieldNames extracts all database field names from a struct based on the `db` tags using sqlx.
@@ -108,3 +110,77 @@ func OnConflictDoNothing(dialect string, columnNoop string) string {
108110 return ` ON CONFLICT DO NOTHING `
109111 }
110112}
113+
114+ // ExtractSchemeFromDSN returns the scheme (e.g. `mysql`, `postgres`, etc) component in a DSN string,
115+ // as well as the remaining part of the DSN after the scheme separator.
116+ // It is an error to not have a scheme present.
117+ // This makes sense in the context of a DSN to be able to identify which database is in use.
118+ func ExtractSchemeFromDSN (dsn string ) (string , string , error ) {
119+ scheme , afterSchemeSeparator , schemeSeparatorFound := strings .Cut (dsn , "://" )
120+ if ! schemeSeparatorFound {
121+ return "" , "" , errors .New ("invalid DSN: missing scheme separator" )
122+ }
123+ if scheme == "" {
124+ return "" , "" , errors .New ("invalid DSN: empty scheme" )
125+ }
126+
127+ return scheme , afterSchemeSeparator , nil
128+ }
129+
130+ // ReplaceSchemeInDSN replaces the scheme (e.g. `mysql`, `postgres`, etc) in a DSN string with another one.
131+ // This is necessary for example when using `cockroach` as a scheme, but using the postgres driver to connect to the database,
132+ // and this driver only accepts `postgres` as a scheme.
133+ func ReplaceSchemeInDSN (dsn string , newScheme string ) (string , error ) {
134+ _ , afterSchemeSeparator , err := ExtractSchemeFromDSN (dsn )
135+ if err != nil {
136+ return "" , errors .WithStack (err )
137+ }
138+
139+ return newScheme + "://" + afterSchemeSeparator , nil
140+ }
141+
142+ // DSNRedacted parses a database DSN and returns a redacted form as a string.
143+ // It replaces any password with "xxxxx" just like `url.Redacted()`.
144+ // Only the password is redacted, not the username.
145+ // This function is necessary because MySQL uses a DSN format not compatible with `url.Parse`.
146+ // Additionally and as a consequence of the point above, the scheme is expected to be present and non-empty.
147+ // This function is less strict that `url.Parse` in the case of MySQL.
148+ // It also does not escape any characters in the username, whereas `url.String()`/`url.Redacted` does.
149+ func DSNRedacted (dsn string ) (string , error ) {
150+ scheme , afterSchemeSeparator , err := ExtractSchemeFromDSN (dsn )
151+ if err != nil {
152+ return "" , errors .WithStack (err )
153+ }
154+
155+ // If this is not MySQL, we simply delegate the work to `url.Parse`.
156+ if scheme != "mysql" {
157+ u , err := url .Parse (dsn )
158+ if err != nil {
159+ return "" , errors .WithStack (err )
160+ }
161+ return u .Redacted (), nil
162+ }
163+
164+ // MySQL has a weird DSN syntax not conforming to a standard URL, of the form:
165+ // `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]`
166+ // We only need to parse up to `@` in order to redact the password. The rest is left as-is.
167+
168+ usernamePassword , afterUsernamePassword , usernamePasswordSeparatorFound := strings .Cut (afterSchemeSeparator , "@" )
169+ if ! usernamePasswordSeparatorFound {
170+ afterUsernamePassword = afterSchemeSeparator
171+ }
172+
173+ username , password , hasPassword := strings .Cut (usernamePassword , ":" )
174+ // We only insert a redacted password in the final result if a password was provided in the input.
175+ // This behavior matches the one of `url.Redacted()`.
176+ if hasPassword {
177+ password = ":xxxxx"
178+ }
179+
180+ res := scheme + "://"
181+ if usernamePasswordSeparatorFound {
182+ res += username + password + "@"
183+ }
184+ res += afterUsernamePassword
185+ return res , nil
186+ }
0 commit comments