From b6709c2717d13ae437cf26375a065e0d674745c0 Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Mon, 20 Apr 2026 23:59:32 -0700 Subject: [PATCH] fix: stop AttributesFromDSN panicking on unix-socket DSNs When the DSN uses the protocol(addr) form with a unix-socket path inside the parentheses (e.g. 'unix(/tmp/mysql.sock)/dbname'), AttributesFromDSN used to split on '/' before looking for the wrapping parens. That produced dsn[:pathIndex] = 'unix(' and caused an out-of-range slice panic further down the chain when net.SplitHostPort chewed through the unbalanced parenthesis (#624). Extract the address-extraction logic into addrFromDSN so the main function stays under the project's cyclop max-complexity budget, and check for the '(...)' wrapper first. If the parens are present and balanced, use the substring between them as the address; otherwise fall back to the previous 'trim at first /' behaviour so bare host[:port] and host:port/db forms keep the same semantics. Closes #624 Signed-off-by: SAY-5 --- CHANGELOG.md | 1 + helpers.go | 77 +++++++++++++++++++++++++------------------------ helpers_test.go | 8 +++++ 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b84813..ac3077f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Fixed - Implement `driver.Validator` on `otConn` so that `database/sql` connection pool health checks are properly delegated to the underlying driver connection. (#619) +- `AttributesFromDSN` no longer panics on DSNs that wrap a unix-socket path in the `protocol(/path/to.sock)` form, e.g. `unix(/tmp/mysql.sock)/dbname`. The parser now extracts the address from inside the parentheses before splitting on `/`. (#625) ## [0.42.0] - 2026-03-30 diff --git a/helpers.go b/helpers.go index 36582292..48497a0c 100644 --- a/helpers.go +++ b/helpers.go @@ -26,46 +26,14 @@ import ( // AttributesFromDSN returns attributes extracted from a DSN string. // It makes the best effort to retrieve values for [semconv.ServerAddressKey] and [semconv.ServerPortKey]. func AttributesFromDSN(dsn string) []attribute.KeyValue { - // [scheme://][user[:password]@][protocol([addr])]/dbname[?param1=value1¶mN=valueN] - // Find the schema part. - schemaIndex := strings.Index(dsn, "://") - if schemaIndex != -1 { - // Remove the schema part from the DSN. - dsn = dsn[schemaIndex+3:] - } - - // [user[:password]@][protocol([addr])]/dbname[?param1=value1¶mN=valueN] - // Find credentials part. - atIndex := strings.Index(dsn, "@") - if atIndex != -1 { - // Remove the credential part from the DSN. - dsn = dsn[atIndex+1:] - } - - // [protocol([addr])]/dbname[?param1=value1¶mN=valueN] - // Find the '/' that separates the address part from the database part. - pathIndex := strings.Index(dsn, "/") - if pathIndex != -1 { - // Remove the path part from the DSN. - dsn = dsn[:pathIndex] - } - - // [protocol([addr])] or [addr] - // Find the '(' that starts the address part. - openParen := strings.Index(dsn, "(") - if openParen != -1 { - // Remove the protocol part from the DSN. - dsn = dsn[openParen+1 : len(dsn)-1] - } - - // [addr] - if len(dsn) == 0 { + addr := addrFromDSN(dsn) + if addr == "" { return nil } - host, portStr, err := net.SplitHostPort(dsn) + host, portStr, err := net.SplitHostPort(addr) if err != nil { - host = dsn + host = addr } attrs := make([]attribute.KeyValue, 0, 2) @@ -74,11 +42,44 @@ func AttributesFromDSN(dsn string) []attribute.KeyValue { } if portStr != "" { - port, err := strconv.ParseInt(portStr, 10, 64) - if err == nil { + if port, err := strconv.ParseInt(portStr, 10, 64); err == nil { attrs = append(attrs, semconv.ServerPortKey.Int64(port)) } } return attrs } + +// addrFromDSN extracts the network address (host[:port] or unix-socket path) +// from a DSN string, stripping any scheme, credentials, protocol wrapper, +// and trailing dbname/query components. +func addrFromDSN(dsn string) string { + // [scheme://][user[:password]@][protocol([addr])]/dbname[?param1=value1¶mN=valueN] + // Strip scheme. + if i := strings.Index(dsn, "://"); i != -1 { + dsn = dsn[i+3:] + } + + // Strip credentials. + if i := strings.Index(dsn, "@"); i != -1 { + dsn = dsn[i+1:] + } + + // If the DSN uses the protocol(addr) form, extract addr from between + // the parens first. Splitting on '/' up front would break on addresses + // like unix(/tmp/mysql.sock), which contain a '/' inside the parens + // and used to trigger an out-of-range slice panic (#624). + openParen := strings.Index(dsn, "(") + + closeParen := strings.Index(dsn, ")") + if openParen != -1 && closeParen > openParen { + return dsn[openParen+1 : closeParen] + } + + // Bare address form: addr/db?params. Trim the path suffix. + if i := strings.Index(dsn, "/"); i != -1 { + return dsn[:i] + } + + return dsn +} diff --git a/helpers_test.go b/helpers_test.go index a8f549e0..786d86be 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -137,6 +137,14 @@ func TestAttributesFromDSN(t *testing.T) { semconv.ServerAddress("tcp"), }, }, + { + // Unix domain socket DSN: the address inside unix(...) contains '/', + // which previously made the naive "split on first /" logic panic. + dsn: "username:password@unix(/tmp/mysql.sock)/mysql?parseTime=true", + expected: []attribute.KeyValue{ + semconv.ServerAddress("/tmp/mysql.sock"), + }, + }, } for _, tc := range testCases {