Copilot commented on code in PR #1468:
URL: https://github.com/apache/pulsar-client-go/pull/1468#discussion_r2963442362


##########
pulsar/internal/service_uri.go:
##########
@@ -55,109 +57,106 @@ func NewPulsarServiceURIFromURIString(uri string) 
(*PulsarServiceURI, error) {
        return u, nil
 }
 
-func NewPulsarServiceURIFromURL(url *url.URL) (*PulsarServiceURI, error) {
-       u, err := fromURL(url)
-       if err != nil {
-               log.Error(err)
-               return nil, err
+func (p *PulsarServiceURI) UseTLS() bool {
+       return p.ServiceName == HTTPSService || slices.Contains(p.ServiceInfos, 
SSLService)
+}
+
+func (p *PulsarServiceURI) PrimaryHostName() (string, error) {
+       if len(p.ServiceHosts) > 0 {
+               host, _, err := net.SplitHostPort(p.ServiceHosts[0])
+               if err != nil {
+                       return "", err
+               }
+               return host, nil
        }
-       return u, nil
+
+       return "", errors.New("no hosts available in ServiceHosts")
+}
+
+func (p *PulsarServiceURI) IsHTTP() bool {
+       return strings.HasPrefix(p.ServiceName, HTTPService)
 }
 
 func fromString(uriStr string) (*PulsarServiceURI, error) {
-       if uriStr == "" || len(uriStr) == 0 {
-               return nil, errors.New("service uriStr string is null")
+       if uriStr == "" {
+               return nil, errors.New("service URI cannot be empty")
        }
-       if strings.Contains(uriStr, "[") && strings.Contains(uriStr, "]") {
-               // deal with ipv6 address
-               hosts := strings.FieldsFunc(uriStr, splitURI)
-               if len(hosts) > 1 {
-                       // deal with ipv6 address
-                       firstHost := hosts[0]
-                       lastHost := hosts[len(hosts)-1]
-                       hasPath := strings.Contains(lastHost, "/")
-                       path := ""
-                       if hasPath {
-                               idx := strings.Index(lastHost, "/")
-                               path = lastHost[idx:]
-                       }
-                       firstHost += path
-                       url, err := url.Parse(firstHost)
-                       if err != nil {
-                               return nil, err
-                       }
-                       serviceURI, err := fromURL(url)
-                       if err != nil {
-                               return nil, err
-                       }
-                       var mHosts []string
-                       var multiHosts []string
-                       mHosts = append(mHosts, serviceURI.ServiceHosts[0])
-                       mHosts = append(mHosts, hosts[1:]...)
-
-                       for _, v := range mHosts {
-                               h, err := 
validateHostName(serviceURI.ServiceName, serviceURI.ServiceInfos, v)
-                               if err == nil {
-                                       multiHosts = append(multiHosts, h)
-                               } else {
-                                       return nil, err
-                               }
-                       }
 
-                       return &PulsarServiceURI{
-                               serviceURI.ServiceName,
-                               serviceURI.ServiceInfos,
-                               multiHosts,
-                               serviceURI.servicePath,
-                               serviceURI.URL,
-                       }, nil
+       // 1. Find first host delimiter (, or ;)
+       firstDelimIdx := strings.IndexFunc(uriStr, splitURI)
+
+       var singleHostURI string
+       var additionalHosts string
+
+       if firstDelimIdx >= 0 {
+               remainder := uriStr[firstDelimIdx:]
+               endIdx := strings.IndexAny(remainder, "/?#")

Review Comment:
   fromString() uses strings.IndexFunc(uriStr, splitURI) to detect multi-host 
delimiters, but this scans the *entire* URI, so a ',' or ';' in the 
path/query/fragment would be misinterpreted as a host delimiter and break 
parsing. The delimiter search should be limited to the authority/host portion 
(between "//" and the first "/?#").



##########
pulsar/internal/service_name_resolver_test.go:
##########
@@ -22,52 +22,53 @@ import (
        "testing"
 
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
 )
 
 func TestResolveBeforeUpdateServiceUrl(t *testing.T) {
-       resolver := NewPulsarServiceNameResolver(nil)
+       resolver, err := NewPulsarServiceNameResolver("")
+       require.NoError(t, err)
        u, err := resolver.ResolveHost()
        assert.Nil(t, u)
        assert.NotNil(t, err)
        assert.EqualError(t, err, "no service url is provided yet")
 }
 
-func TestResolveUriBeforeUpdateServiceUrl(t *testing.T) {
-       resolver := NewPulsarServiceNameResolver(nil)
-       u, err := resolver.ResolveHostURI()
+func TestResolveBeforeUpdateServiceUrlReturnsErrorWithoutServiceURL(t 
*testing.T) {
+       resolver, err := NewPulsarServiceNameResolver("")
+       require.NoError(t, err)
+       u, err := resolver.ResolveHost()
        assert.Nil(t, u)
        assert.NotNil(t, err)
        assert.EqualError(t, err, "no service url is provided yet")
 }

Review Comment:
   TestResolveBeforeUpdateServiceUrl and 
TestResolveBeforeUpdateServiceUrlReturnsErrorWithoutServiceURL are currently 
identical (both call ResolveHost without setting a service URL and assert the 
same error). Keeping both provides no additional coverage; consider removing 
one or changing one to cover a distinct behavior.



##########
pulsar/client_impl.go:
##########
@@ -80,31 +79,31 @@ func newClient(options ClientOptions) (Client, error) {
                return nil, newError(InvalidConfiguration, "URL is required for 
client")
        }
 
-       url, err := url.Parse(options.URL)
+       pulsarServiceURI, err := 
internal.NewPulsarServiceURIFromURIString(options.URL)
        if err != nil {
                logger.WithError(err).Error("Failed to parse service URL")
                return nil, newError(InvalidConfiguration, "Invalid service 
URL")
        }

Review Comment:
   newClient() used to return a specific InvalidConfiguration error for 
unsupported schemes (e.g., "Invalid URL scheme 'ftp'"). After switching to 
NewPulsarServiceURIFromURIString, unsupported schemes now fall into the generic 
"Invalid service URL" path, which is less actionable for users. Consider 
preserving the explicit scheme validation/error (e.g., by extracting the scheme 
before parsing hosts) so callers still get a clear unsupported-scheme message.



##########
pulsar/internal/service_uri.go:
##########
@@ -166,42 +165,69 @@ func splitURI(r rune) bool {
 }
 
 func validateHostName(serviceName string, serviceInfos []string, hostname 
string) (string, error) {
-       uri, err := url.Parse("dummyscheme://" + hostname)
-       if err != nil {
-               return "", err
-       }
-       host := uri.Hostname()
-       if strings.Contains(hostname, "[") && strings.Contains(hostname, "]") {
-               host = fmt.Sprintf("[%s]", host)
-       }
-       if host == "" || uri.Scheme == "" {
-               return "", errors.New("Invalid hostname : " + hostname)
+       var host, port string
+
+       // Attempt to split host and port using the standard library.
+       //
+       // net.SplitHostPort enforces strict host:port syntax:
+       //   - IPv4: "127.0.0.1:6650"
+       //   - IPv6: "[fec0::1]:6650"
+       //
+       // It will fail for:
+       //   - hosts without a port
+       //   - bare IPv6 literals without brackets (e.g. "fec0::1")
+       host, port, err := net.SplitHostPort(hostname)
+       if err == nil && host == "" {
+               // net.SplitHostPort accepts ":port" with an empty host, but we 
explicitly
+               // reject such inputs because a non-empty hostname is required.
+               return "", fmt.Errorf("invalid address: host is empty in %q", 
hostname)
        }
 
-       port := uri.Port()
-       if uri.Port() == "" {
+       if err != nil {
+               // If the hostname contains ':' but is not bracketed, it is 
very likely
+               // an invalid IPv6 literal or an invalid host with too many 
colons.
+               //
+               // Examples rejected here:
+               //   - "fec0::1"
+               //   - "fec0::1:6650"
+               //   - "localhost:6650:6651"
+               if strings.Count(hostname, ":") > 0 && 
!strings.HasPrefix(hostname, "[") {
+                       return "", fmt.Errorf("invalid address (maybe missing 
brackets for IPv6 or too many colons): %s", hostname)
+               }
+
+               // Otherwise, treat the entire hostname as host without an 
explicit port.
+               // In this case, we fall back to the default port derived from 
the service.
+               host = hostname
                p := getServicePort(serviceName, serviceInfos)
                if p == -1 {
-                       return "", fmt.Errorf("invalid port : %d", p)
+                       return "", fmt.Errorf("no port found")
                }
-               port = fmt.Sprint(p)
+               port = strconv.Itoa(p)
        }
-       result := host + ":" + port
-       _, _, err = net.SplitHostPort(result)
-       if err != nil {
-               return "", err
+
+       // Remove surrounding brackets if present.
+       //
+       // Note:
+       //   "[fec0::1]" is NOT an IPv6 address itself, but a URI/host syntax
+       //   representation used to disambiguate IPv6 literals when ports are 
involved.
+       //   We strip brackets here so the address can be validated and 
normalized.
+       cleanHost := host
+       if len(host) >= 2 && host[0] == '[' && host[len(host)-1] == ']' {
+               cleanHost = host[1 : len(host)-1]
        }
-       return result, nil
+
+       return net.JoinHostPort(cleanHost, port), nil

Review Comment:
   validateHostName() strips brackets and then returns 
net.JoinHostPort(cleanHost, port) without validating that a bracketed host is 
actually an IPv6 literal. As a result, inputs like "pulsar://[example]:6650" 
will be accepted (and normalized to "example:6650"), contradicting the new 
invalid-URI test case. Add an explicit check that any host provided in brackets 
parses as IPv6 (optionally handling zone IDs), and reject bracketed non-IPv6 
hosts.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to