Repository: C2FO/vfs
Pull request #192 (2024-06-18 08:16:09)

Add uri authority encode utils 191
https://github.com/C2FO/vfs/issues/191


Patch:
diff --git a/.gvmrc b/.gvmrc
index 3c9a034..5e7f5b7 100644
--- a/.gvmrc
+++ b/.gvmrc
@@ -1,1 +1,1 @@
-go1.22.0
+go1.22.4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02d1abd..10f3aab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 ## [Unreleased]
 
-## [6.14.3] - 2024-05-30
+## [6.15.0] - 2024-06-18
+### Added
+- Fixed #191 - Add a util functions to encode authority strictly following RFC 3986.
+
+## [6.14.3] - 2024-06-11
 ### Fixed
 - Fixed #189 - Update utils authority package to handle proper encoding/decoding of uri with reserved characters.
 
diff --git a/utils/authority.go b/utils/authority.go
index 1e6c712..18eda87 100644
--- a/utils/authority.go
+++ b/utils/authority.go
@@ -159,3 +159,67 @@ func validOptionalPort(port string) bool {
 	}
 	return true
 }
+
+// EncodeUserInfo takes an unencoded URI authority userinfo string and encodes it
+func EncodeUserInfo(rawUserInfo string) string {
+	parts := strings.SplitN(rawUserInfo, ":", 2)
+	encodedParts := make([]string, len(parts))
+	for i, part := range parts {
+		encoded := url.QueryEscape(part)
+		decoded := strings.NewReplacer(
+			"%21", "!", "%24", "$", "%26", "&", "%27", "'",
+			"%28", "(", "%29", ")", "%2A", "*", "%2B", "+",
+			"%2C", ",", "%3B", ";", "%3D", "=",
+		).Replace(encoded)
+		encodedParts[i] = decoded
+	}
+	return strings.Join(encodedParts, ":")
+}
+
+// EncodeAuthority takes an unencoded URI authority string and encodes it
+func EncodeAuthority(rawAuthority string) (string, error) {
+	var userInfo, hostPort string
+
+	// Split the authority into user info and hostPort
+	atIndex := strings.LastIndex(rawAuthority, "@")
+	if atIndex != -1 {
+		userInfo = rawAuthority[:atIndex]
+		hostPort = rawAuthority[atIndex+1:]
+	} else {
+		hostPort = rawAuthority
+	}
+
+	// Encode userInfo if present
+	if userInfo != "" {
+		userInfo = EncodeUserInfo(userInfo)
+	}
+
+	// Split host and port
+	var host, port string
+	hostPortSplit := strings.SplitN(hostPort, ":", 2)
+	if len(hostPortSplit) > 0 {
+		host = hostPortSplit[0]
+	}
+	if len(hostPortSplit) > 1 {
+		port = hostPortSplit[1]
+	}
+
+	// Encode host and port
+	encodedHost := url.QueryEscape(host)
+	var encodedPort string
+	if port != "" {
+		encodedPort = url.QueryEscape(port)
+	}
+
+	// Reconstruct the encoded authority string
+	var encodedAuthority string
+	if userInfo != "" {
+		encodedAuthority = userInfo + "@"
+	}
+	encodedAuthority += encodedHost
+	if encodedPort != "" {
+		encodedAuthority += ":" + encodedPort
+	}
+
+	return encodedAuthority, nil
+}


Test patch:
diff --git a/utils/authority_test.go b/utils/authority_test.go
index d72a725..f55e243 100644
--- a/utils/authority_test.go
+++ b/utils/authority_test.go
@@ -24,7 +24,7 @@ type authorityTest struct {
 }
 
 func (a *authoritySuite) TestAuthority() {
-	tests := []*authorityTest{
+	tests := []authorityTest{
 		{
 			authorityString: "",
 			host:            "",
@@ -308,6 +308,80 @@ func (a *authoritySuite) TestAuthority() {
 	}
 }
 
+type encodeAuthorityTest struct {
+	rawAuthority    string
+	expectedEncoded string
+	hasError        bool
+	errMessage      string
+	message         string
+}
+
+func (a *authoritySuite) TestEncodeAuthority() {
+	tests := []encodeAuthorityTest{
+		{
+			rawAuthority:    "domain.com\\user@someserver.com:22",
+			expectedEncoded: "domain.com%5Cuser@someserver.com:22",
+			hasError:        false,
+			errMessage:      "",
+			message:         "basic encoding",
+		},
+		{
+			rawAuthority:    "example.com:80",
+			expectedEncoded: "example.com:80",
+			hasError:        false,
+			errMessage:      "",
+			message:         "no user info",
+		},
+		{
+			rawAuthority:    "user:password@host.com",
+			expectedEncoded: "user:password@host.com",
+			hasError:        false,
+			errMessage:      "",
+			message:         "username and password",
+		},
+		{
+			rawAuthority:    "!username@host.com:22",
+			expectedEncoded: "!username@host.com:22",
+			hasError:        false,
+			errMessage:      "",
+			message:         "exclamation point in username (remains unencoded)",
+		},
+		{
+			rawAuthority:    "user@host.com",
+			expectedEncoded: "user@host.com",
+			hasError:        false,
+			errMessage:      "",
+			message:         "username only",
+		},
+		{
+			rawAuthority:    "host.com:8080",
+			expectedEncoded: "host.com:8080",
+			hasError:        false,
+			errMessage:      "",
+			message:         "host and port only",
+		},
+		{
+			rawAuthority:    "@host.com",
+			expectedEncoded: "host.com",
+			hasError:        false,
+			errMessage:      "",
+			message:         "empty user info",
+		},
+	}
+
+	for _, t := range tests {
+		a.Run(t.message, func() {
+			actual, err := EncodeAuthority(t.rawAuthority)
+			if t.hasError {
+				a.ErrorContains(err, t.errMessage, t.message)
+			} else {
+				a.NoError(err, t.message)
+				a.Equal(t.expectedEncoded, actual, t.message)
+			}
+		})
+	}
+}
+
 func TestAuthority(t *testing.T) {
 	suite.Run(t, new(authoritySuite))
 }