Repository: valyala/fasthttp
Pull request #654 (2019-09-17 07:15:11)

Improve appendQuotedPath and AppendQuotedArg
This implementation similar to the official implementation but more faster, and it also fix some problems. See the previous pull request in https://github.com/valyala/fasthttp/pull/653 


Patch:
diff --git a/bytesconv.go b/bytesconv.go
index 7a98031..e0ca3ab 100644
--- a/bytesconv.go
+++ b/bytesconv.go
@@ -393,6 +393,58 @@ func s2b(s string) (b []byte) {
 	return b
 }
 
+var quotedArgShouldEscapeTable = func() [256]bool {
+	// According to RFC 3986 §2.3
+	var a [256]bool
+	for i := 0; i < 256; i++ {
+		a[i] = true
+	}
+
+	// ALPHA
+	for i := int('a'); i <= int('z'); i++ {
+		a[i] = false
+	}
+	for i := int('A'); i <= int('Z'); i++ {
+		a[i] = false
+	}
+
+	// DIGIT
+	for i := int('0'); i <= int('9'); i++ {
+		a[i] = false
+	}
+
+	// Unreserved characters
+	a[int('-')] = false
+	a[int('_')] = false
+	a[int('.')] = false
+	a[int('~')] = false
+
+	return a
+}()
+
+var quotedPathShouldEscapeTable = func() [256]bool {
+	// The implementation here equal to net/url shouldEscape(s, encodePath)
+	//
+	// The RFC allows : @ & = + $ but saves / ; , for assigning
+	// meaning to individual path segments. This package
+	// only manipulates the path as a whole, so we allow those
+	// last three as well. That leaves only ? to escape.
+	var a = quotedArgShouldEscapeTable
+
+	// '$', '&', '+', ',', '/', ':', ';', '=', '@'
+	a[int('$')] = false
+	a[int('&')] = false
+	a[int('+')] = false
+	a[int(',')] = false
+	a[int('/')] = false
+	a[int(':')] = false
+	a[int(';')] = false
+	a[int('=')] = false
+	a[int('@')] = false
+
+	return a
+}()
+
 // AppendUnquotedArg appends url-decoded src to dst and returns appended dst.
 //
 // dst may point to src. In this case src will be overwritten.
@@ -403,33 +455,29 @@ func AppendUnquotedArg(dst, src []byte) []byte {
 // AppendQuotedArg appends url-encoded src to dst and returns appended dst.
 func AppendQuotedArg(dst, src []byte) []byte {
 	for _, c := range src {
-		// See http://www.w3.org/TR/html5/forms.html#form-submission-algorithm
-		if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' ||
-			c == '*' || c == '-' || c == '.' || c == '_' {
-			dst = append(dst, c)
-		} else {
+		switch {
+		case c == ' ':
+			dst = append(dst, '+')
+		case quotedArgShouldEscapeTable[int(c)]:
 			dst = append(dst, '%', hexCharUpper(c>>4), hexCharUpper(c&15))
+		default:
+			dst = append(dst, c)
 		}
 	}
 	return dst
 }
 
 func appendQuotedPath(dst, src []byte) []byte {
+	// Fix issue in https://github.com/golang/go/issues/11202
+	if len(src) == 1 && src[0] == '*' {
+		return append(dst, '*')
+	}
+
 	for _, c := range src {
-		// From the spec: http://tools.ietf.org/html/rfc3986#section-3.3
-		// an path can contain zero or more of pchar that is defined as follows:
-		// pchar       = unreserved / pct-encoded / sub-delims / ":" / "@"
-		// pct-encoded = "%" HEXDIG HEXDIG
-		// unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
-		// sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
-		//             / "*" / "+" / "," / ";" / "="
-		if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' ||
-			c == '-' || c == '.' || c == '_' || c == '~' || c == '!' || c == '$' ||
-			c == '&' || c == '\'' || c == '(' || c == ')' || c == '*' || c == '+' ||
-			c == ',' || c == ';' || c == '=' || c == ':' || c == '@' || c == '/' {
-			dst = append(dst, c)
-		} else {
+		if quotedPathShouldEscapeTable[int(c)] {
 			dst = append(dst, '%', hexCharUpper(c>>4), hexCharUpper(c&15))
+		} else {
+			dst = append(dst, c)
 		}
 	}
 	return dst
diff --git a/uri.go b/uri.go
index e9cd4b1..913c86b 100644
--- a/uri.go
+++ b/uri.go
@@ -48,6 +48,15 @@ type URI struct {
 	queryArgs       Args
 	parsedQueryArgs bool
 
+	// Path values are sent as-is without normalization
+	//
+	// Disabled path normalization may be useful for proxying incoming requests
+	// to servers that are expecting paths to be forwarded as-is.
+	//
+	// By default path values are normalized, i.e.
+	// extra slashes are removed, special characters are encoded.
+	DisablePathNormalizing bool
+
 	fullURI    []byte
 	requestURI []byte
 
@@ -71,6 +80,7 @@ func (u *URI) CopyTo(dst *URI) {
 
 	u.queryArgs.CopyTo(&dst.queryArgs)
 	dst.parsedQueryArgs = u.parsedQueryArgs
+	dst.DisablePathNormalizing = u.DisablePathNormalizing
 
 	// fullURI and requestURI shouldn't be copied, since they are created
 	// from scratch on each FullURI() and RequestURI() call.
@@ -215,6 +225,7 @@ func (u *URI) Reset() {
 	u.host = u.host[:0]
 	u.queryArgs.Reset()
 	u.parsedQueryArgs = false
+	u.DisablePathNormalizing = false
 
 	// There is no need in u.fullURI = u.fullURI[:0], since full uri
 	// is calculated on each call to FullURI().
@@ -387,7 +398,12 @@ func normalizePath(dst, src []byte) []byte {
 
 // RequestURI returns RequestURI - i.e. URI without Scheme and Host.
 func (u *URI) RequestURI() []byte {
-	dst := appendQuotedPath(u.requestURI[:0], u.Path())
+	var dst []byte
+	if u.DisablePathNormalizing {
+		dst = append(u.requestURI[:0], u.PathOriginal()...)
+	} else {
+		dst = appendQuotedPath(u.requestURI[:0], u.Path())
+	}
 	if u.queryArgs.Len() > 0 {
 		dst = append(dst, '?')
 		dst = u.queryArgs.AppendBytes(dst)


Test patch:
diff --git a/args_test.go b/args_test.go
index 1502414..d6df55d 100644
--- a/args_test.go
+++ b/args_test.go
@@ -3,6 +3,7 @@ package fasthttp
 import (
 	"bytes"
 	"fmt"
+	"net/url"
 	"reflect"
 	"strings"
 	"testing"
@@ -170,8 +171,16 @@ func TestArgsPeekMulti(t *testing.T) {
 
 func TestArgsEscape(t *testing.T) {
 	testArgsEscape(t, "foo", "bar", "foo=bar")
-	testArgsEscape(t, "f.o,1:2/4", "~`!@#$%^&*()_-=+\\|/[]{};:'\"<>,./?",
-		"f.o%2C1%3A2%2F4=%7E%60%21%40%23%24%25%5E%26*%28%29_-%3D%2B%5C%7C%2F%5B%5D%7B%7D%3B%3A%27%22%3C%3E%2C.%2F%3F")
+
+	// Test all characters
+	k := "f.o,1:2/4"
+	var v = make([]byte, 256)
+	for i := 0; i < 256; i++ {
+		v[i] = byte(i)
+	}
+	u := url.Values{}
+	u.Add(k, string(v))
+	testArgsEscape(t, k, string(v), u.Encode())
 }
 
 func testArgsEscape(t *testing.T, k, v, expectedS string) {
@@ -183,6 +192,30 @@ func testArgsEscape(t *testing.T, k, v, expectedS string) {
 	}
 }
 
+func TestPathEscape(t *testing.T) {
+	testPathEscape(t, "/foo/bar")
+	testPathEscape(t, "")
+	testPathEscape(t, "/")
+	testPathEscape(t, "//")
+	testPathEscape(t, "*") // See https://github.com/golang/go/issues/11202
+
+	// Test all characters
+	var pathSegment = make([]byte, 256)
+	for i := 0; i < 256; i++ {
+		pathSegment[i] = byte(i)
+	}
+	testPathEscape(t, "/foo/"+string(pathSegment))
+}
+
+func testPathEscape(t *testing.T, s string) {
+	u := url.URL{Path: s}
+	expectedS := u.EscapedPath()
+	res := string(appendQuotedPath(nil, []byte(s)))
+	if res != expectedS {
+		t.Fatalf("unexpected args %q. Expecting %q.", res, expectedS)
+	}
+}
+
 func TestArgsWriteTo(t *testing.T) {
 	s := "foo=bar&baz=123&aaa=bbb"
 
@@ -344,7 +377,7 @@ func TestArgsString(t *testing.T) {
 	testArgsString(t, &a, "foo=bar")
 	testArgsString(t, &a, "foo=bar&baz=sss")
 	testArgsString(t, &a, "")
-	testArgsString(t, &a, "f%20o=x.x*-_8x%D0%BF%D1%80%D0%B8%D0%B2%D0%B5aaa&sdf=ss")
+	testArgsString(t, &a, "f+o=x.x%2A-_8x%D0%BF%D1%80%D0%B8%D0%B2%D0%B5aaa&sdf=ss")
 	testArgsString(t, &a, "=asdfsdf")
 }
 
diff --git a/uri_test.go b/uri_test.go
index 7ed05f9..84f9380 100644
--- a/uri_test.go
+++ b/uri_test.go
@@ -184,6 +184,16 @@ func testURIPathNormalize(t *testing.T, u *URI, requestURI, expectedPath string)
 	}
 }
 
+func TestURINoNormalization(t *testing.T) {
+	var u URI
+	irregularPath := "/aaa%2Fbbb%2F%2E.%2Fxxx"
+	u.Parse(nil, []byte(irregularPath))
+	u.DisablePathNormalizing = true
+	if string(u.RequestURI()) != irregularPath {
+		t.Fatalf("Unexpected path %q. Expected %q.", u.Path(), irregularPath)
+	}
+}
+
 func TestURICopyTo(t *testing.T) {
 	var u URI
 	var copyU URI