Repository: valyala/fasthttp
Pull request #1684 (2023-12-29 09:39:57)

Change empty string checks to be more idiomatic
This PR refactors code by changing `if len(str) == 0 {` to more idiomatic `if str == "" {`. Additionally, enables gocritic's `emptyStringTest` check to prevent such code in the future.

See ["Empty string check"](https://dmitri.shuralyov.com/idiomatic-go#empty-string-check).


Patch:
diff --git a/.gitignore b/.gitignore
index 035e302..7673684 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ tags
 .vscode
 .DS_Store
 vendor/
+testdata/fuzz
diff --git a/.golangci.yml b/.golangci.yml
index d768d90..6d91345 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -68,6 +68,9 @@ linters-settings:
       "all",
       "-ST1000", # at least one file in a package should have a package comment
     ]
+  gocritic:
+    enabled-checks:
+      - emptyStringTest
 
 issues:
   # Show all issues from a linter.
diff --git a/client.go b/client.go
index 122894b..b5493e9 100644
--- a/client.go
+++ b/client.go
@@ -1825,7 +1825,7 @@ func newClientTLSConfig(c *tls.Config, addr string) *tls.Config {
 		c = c.Clone()
 	}
 
-	if len(c.ServerName) == 0 {
+	if c.ServerName == "" {
 		serverName := tlsServerName(addr)
 		if serverName == "*" {
 			c.InsecureSkipVerify = true
diff --git a/expvarhandler/expvar.go b/expvarhandler/expvar.go
index 796954d..b750261 100644
--- a/expvarhandler/expvar.go
+++ b/expvarhandler/expvar.go
@@ -53,7 +53,7 @@ func ExpvarHandler(ctx *fasthttp.RequestCtx) {
 
 func getExpvarRegexp(ctx *fasthttp.RequestCtx) (*regexp.Regexp, error) {
 	r := string(ctx.QueryArgs().Peek("r"))
-	if len(r) == 0 {
+	if r == "" {
 		return defaultRE, nil
 	}
 	rr, err := regexp.Compile(r)
diff --git a/fs.go b/fs.go
index 4a8e0f2..f35f22c 100644
--- a/fs.go
+++ b/fs.go
@@ -100,7 +100,7 @@ func ServeFile(ctx *RequestCtx, path string) {
 		rootFSHandler = rootFS.NewRequestHandler()
 	})
 
-	if len(path) == 0 || !filepath.IsAbs(path) {
+	if path == "" || !filepath.IsAbs(path) {
 		// extend relative path to absolute path
 		hasTrailingSlash := len(path) > 0 && (path[len(path)-1] == '/' || path[len(path)-1] == '\\')
 
@@ -429,7 +429,7 @@ func (fs *FS) normalizeRoot(root string) string {
 	// fs.FS uses relative paths, that paths are slash-separated on all systems, even Windows.
 	if fs.FS == nil {
 		// Serve files from the current working directory if Root is empty or if Root is a relative path.
-		if (!fs.AllowEmptyRoot && len(root) == 0) || (len(root) > 0 && !filepath.IsAbs(root)) {
+		if (!fs.AllowEmptyRoot && root == "") || (root != "" && !filepath.IsAbs(root)) {
 			path, err := os.Getwd()
 			if err != nil {
 				path = "."
@@ -452,14 +452,14 @@ func (fs *FS) initRequestHandler() {
 	root := fs.normalizeRoot(fs.Root)
 
 	compressRoot := fs.CompressRoot
-	if len(compressRoot) == 0 {
+	if compressRoot == "" {
 		compressRoot = root
 	} else {
 		compressRoot = fs.normalizeRoot(compressRoot)
 	}
 
 	compressedFileSuffixes := fs.CompressedFileSuffixes
-	if len(compressedFileSuffixes["br"]) == 0 || len(compressedFileSuffixes["gzip"]) == 0 ||
+	if compressedFileSuffixes["br"] == "" || compressedFileSuffixes["gzip"] == "" ||
 		compressedFileSuffixes["br"] == compressedFileSuffixes["gzip"] {
 		// Copy global map
 		compressedFileSuffixes = make(map[string]string, len(FSCompressedFileSuffixes))
@@ -1474,7 +1474,7 @@ func (h *fsHandler) newCompressedFSFileCache(f fs.File, fileInfo fs.FileInfo, fi
 
 	ext := fileExtension(fileInfo.Name(), false, h.compressedFileSuffixes[fileEncoding])
 	contentType := mime.TypeByExtension(ext)
-	if len(contentType) == 0 {
+	if contentType == "" {
 		data, err := readFileHeader(f, false, fileEncoding)
 		if err != nil {
 			return nil, fmt.Errorf("cannot read header of the file %q: %w", fileInfo.Name(), err)
@@ -1573,7 +1573,7 @@ func (h *fsHandler) newFSFile(f fs.File, fileInfo fs.FileInfo, compressed bool,
 	// detect content-type
 	ext := fileExtension(fileInfo.Name(), compressed, h.compressedFileSuffixes[fileEncoding])
 	contentType := mime.TypeByExtension(ext)
-	if len(contentType) == 0 {
+	if contentType == "" {
 		data, err := readFileHeader(f, compressed, fileEncoding)
 		if err != nil {
 			return nil, fmt.Errorf("cannot read header of the file %q: %w", fileInfo.Name(), err)
diff --git a/fuzzit/cookie/cookie_fuzz.go b/fuzzit/cookie/cookie_fuzz.go
deleted file mode 100644
index 929513f..0000000
--- a/fuzzit/cookie/cookie_fuzz.go
+++ /dev/null
@@ -1,25 +0,0 @@
-//go:build gofuzz
-
-package fuzz
-
-import (
-	"bytes"
-
-	"github.com/valyala/fasthttp"
-)
-
-func Fuzz(data []byte) int {
-	c := fasthttp.AcquireCookie()
-	defer fasthttp.ReleaseCookie(c)
-
-	if err := c.ParseBytes(data); err != nil {
-		return 0
-	}
-
-	w := bytes.Buffer{}
-	if _, err := c.WriteTo(&w); err != nil {
-		return 0
-	}
-
-	return 1
-}
diff --git a/header.go b/header.go
index 51be74a..16d6a6c 100644
--- a/header.go
+++ b/header.go
@@ -545,6 +545,104 @@ func (h *ResponseHeader) AddTrailerBytes(trailer []byte) error {
 	return err
 }
 
+// validHeaderFieldByte returns true if c is a valid tchar as defined
+// by section 5.6.2 of [RFC9110].
+func validHeaderFieldByte(c byte) bool {
+	return c < 128 && tcharTable[c]
+}
+
+//	 tchar  = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+//			   / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+//			   / DIGIT / ALPHA
+//
+// See: https://www.rfc-editor.org/rfc/rfc9110#tokens
+var tcharTable = [128]bool{
+	'!': true, '#': true, '$': true, '%': true, '&': true, '\'': true, '*': true, '+': true,
+	'-': true, '.': true, '^': true, '_': true, '`': true, '|': true, '~': true,
+	'0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true,
+	'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 'G': true,
+	'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 'M': true, 'N': true,
+	'O': true, 'P': true, 'Q': true, 'R': true, 'S': true, 'T': true, 'U': true,
+	'V': true, 'W': true, 'X': true, 'Y': true, 'Z': true,
+	'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true,
+	'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true,
+	'o': true, 'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true,
+	'v': true, 'w': true, 'x': true, 'y': true, 'z': true,
+}
+
+// VisitHeaderParams calls f for each parameter in the given header bytes.
+// It stops processing when f returns false or an invalid parameter is found.
+// Parameter values may be quoted, in which case \ is treated as an escape
+// character, and the value is unquoted before being passed to value.
+// See: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.6
+//
+// f must not retain references to key and/or value after returning.
+// Copy key and/or value contents before returning if you need retaining them.
+func VisitHeaderParams(b []byte, f func(key, value []byte) bool) {
+	for len(b) > 0 {
+		idxSemi := 0
+		for idxSemi < len(b) && b[idxSemi] != ';' {
+			idxSemi++
+		}
+		if idxSemi >= len(b) {
+			return
+		}
+		b = b[idxSemi+1:]
+		for len(b) > 0 && b[0] == ' ' {
+			b = b[1:]
+		}
+
+		n := 0
+		if len(b) == 0 || !validHeaderFieldByte(b[n]) {
+			return
+		}
+		n++
+		for n < len(b) && validHeaderFieldByte(b[n]) {
+			n++
+		}
+
+		if n >= len(b)-1 || b[n] != '=' {
+			return
+		}
+		param := b[:n]
+		n++
+
+		switch {
+		case validHeaderFieldByte(b[n]):
+			m := n
+			n++
+			for n < len(b) && validHeaderFieldByte(b[n]) {
+				n++
+			}
+			if !f(param, b[m:n]) {
+				return
+			}
+		case b[n] == '"':
+			foundEndQuote := false
+			escaping := false
+			n++
+			m := n
+			for ; n < len(b); n++ {
+				if b[n] == '"' && !escaping {
+					foundEndQuote = true
+					break
+				}
+				escaping = (b[n] == '\\' && !escaping)
+			}
+			if !foundEndQuote {
+				return
+			}
+			if !f(param, b[m:n]) {
+				return
+			}
+			n++
+		default:
+			return
+		}
+		b = b[n:]
+	}
+}
+
 // MultipartFormBoundary returns boundary part
 // from 'multipart/form-data; boundary=...' Content-Type.
 func (h *RequestHeader) MultipartFormBoundary() []byte {
diff --git a/http.go b/http.go
index 4785d12..5672301 100644
--- a/http.go
+++ b/http.go
@@ -958,7 +958,7 @@ func (req *Request) MultipartForm() (*multipart.Form, error) {
 	}
 
 	req.multipartFormBoundary = string(req.Header.MultipartFormBoundary())
-	if len(req.multipartFormBoundary) == 0 {
+	if req.multipartFormBoundary == "" {
 		return nil, ErrNoMultipartForm
 	}
 
@@ -1014,7 +1014,7 @@ func marshalMultipartForm(f *multipart.Form, boundary string) ([]byte, error) {
 func WriteMultipartForm(w io.Writer, f *multipart.Form, boundary string) error {
 	// Do not care about memory allocations here, since multipart
 	// form processing is slow.
-	if len(boundary) == 0 {
+	if boundary == "" {
 		return errors.New("form boundary cannot be empty")
 	}
 
diff --git a/server.go b/server.go
index 95eb2c0..dbee2a4 100644
--- a/server.go
+++ b/server.go
@@ -1736,7 +1736,7 @@ func (s *Server) ServeTLSEmbed(ln net.Listener, certData, keyData []byte) error
 // This function allows programmer to handle multiple domains
 // in one server structure. See examples/multidomain.
 func (s *Server) AppendCert(certFile, keyFile string) error {
-	if len(certFile) == 0 && len(keyFile) == 0 {
+	if certFile == "" && keyFile == "" {
 		return errNoCertOrKeyProvided
 	}
 


Test patch:
diff --git a/cookie_test.go b/cookie_test.go
index b4b81ac..df9568c 100644
--- a/cookie_test.go
+++ b/cookie_test.go
@@ -1,6 +1,7 @@
 package fasthttp
 
 import (
+	"bytes"
 	"strings"
 	"testing"
 	"time"
@@ -15,6 +16,27 @@ func TestCookiePanic(t *testing.T) {
 	}
 }
 
+func FuzzCookieParse(f *testing.F) {
+	inputs := []string{
+		`xxx=yyy`,
+		`xxx=yyy; expires=Tue, 10 Nov 2009 23:00:00 GMT; domain=foobar.com; path=/a/b`,
+		" \n\t\"",
+	}
+	for _, input := range inputs {
+		f.Add([]byte(input))
+	}
+	c := AcquireCookie()
+	defer ReleaseCookie(c)
+	f.Fuzz(func(t *testing.T, cookie []byte) {
+		_ = c.ParseBytes(cookie)
+
+		w := bytes.Buffer{}
+		if _, err := c.WriteTo(&w); err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
+	})
+}
+
 func TestCookieValueWithEqualAndSpaceChars(t *testing.T) {
 	t.Parallel()
 
diff --git a/header_test.go b/header_test.go
index 2893751..83b8a9c 100644
--- a/header_test.go
+++ b/header_test.go
@@ -1061,6 +1061,75 @@ func testRequestHeaderHasAcceptEncoding(t *testing.T, ae, v string, resultExpect
 	}
 }
 
+func TestVisitHeaderParams(t *testing.T) {
+	t.Parallel()
+	testVisitHeaderParams(t, "text/plain;charset=utf-8;q=0.39", [][2]string{{"charset", "utf-8"}, {"q", "0.39"}})
+	testVisitHeaderParams(t, "text/plain;   foo=bar   ;", [][2]string{{"foo", "bar"}})
+	testVisitHeaderParams(t, `text/plain;      foo="bar";   `, [][2]string{{"foo", "bar"}})
+	testVisitHeaderParams(t, `text/plain; foo="text/plain,text/html;charset=\"utf-8\""`, [][2]string{{"foo", `text/plain,text/html;charset=\"utf-8\"`}})
+	testVisitHeaderParams(t, "text/plain foo=bar", [][2]string{})
+	testVisitHeaderParams(t, "text/plain;", [][2]string{})
+	testVisitHeaderParams(t, "text/plain; ", [][2]string{})
+	testVisitHeaderParams(t, "text/plain; foo", [][2]string{})
+	testVisitHeaderParams(t, "text/plain; foo=", [][2]string{})
+	testVisitHeaderParams(t, "text/plain; =bar", [][2]string{})
+	testVisitHeaderParams(t, "text/plain; foo = bar", [][2]string{})
+	testVisitHeaderParams(t, `text/plain; foo="bar`, [][2]string{})
+	testVisitHeaderParams(t, "text/plain;;foo=bar", [][2]string{})
+
+	parsed := make([][2]string, 0)
+	VisitHeaderParams([]byte(`text/plain; foo=bar; charset=utf-8`), func(key, value []byte) bool {
+		parsed = append(parsed, [2]string{string(key), string(value)})
+		return !bytes.Equal(key, []byte("foo"))
+	})
+
+	if len(parsed) != 1 {
+		t.Fatalf("expected 1 HTTP parameter, parsed %v", len(parsed))
+	}
+
+	if parsed[0] != [2]string{"foo", "bar"} {
+		t.Fatalf("unexpected parameter %v=%v. Expecting foo=bar", parsed[0][0], parsed[0][1])
+	}
+}
+
+func testVisitHeaderParams(t *testing.T, header string, expectedParams [][2]string) {
+	parsed := make([][2]string, 0)
+	VisitHeaderParams([]byte(header), func(key, value []byte) bool {
+		parsed = append(parsed, [2]string{string(key), string(value)})
+		return true
+	})
+
+	if len(parsed) != len(expectedParams) {
+		t.Fatalf("expected %v HTTP parameters, parsed %v", len(expectedParams), len(parsed))
+	}
+
+	for i := range expectedParams {
+		if expectedParams[i] != parsed[i] {
+			t.Fatalf("unexpected parameter %v=%v. Expecting %v=%v", parsed[i][0], parsed[i][1], expectedParams[i][0], expectedParams[i][1])
+		}
+	}
+}
+
+func FuzzVisitHeaderParams(f *testing.F) {
+	inputs := []string{
+		`application/json; v=1; foo=bar; q=0.938; param=param; param="big fox"; q=0.43`,
+		`*/*`,
+		`\\`,
+		`text/plain; foo="\\\"\'\\''\'"`,
+	}
+	for _, input := range inputs {
+		f.Add([]byte(input))
+	}
+	f.Fuzz(func(t *testing.T, header []byte) {
+		VisitHeaderParams(header, func(key, value []byte) bool {
+			if len(key) == 0 {
+				t.Errorf("Unexpected length zero parameter, failed input was: %s", header)
+			}
+			return true
+		})
+	})
+}
+
 func TestRequestMultipartFormBoundary(t *testing.T) {
 	t.Parallel()
 
@@ -2286,7 +2355,7 @@ type bufioPeekReader struct {
 }
 
 func (r *bufioPeekReader) Read(b []byte) (int, error) {
-	if len(r.s) == 0 {
+	if r.s == "" {
 		return 0, io.EOF
 	}
 
diff --git a/header_timing_test.go b/header_timing_test.go
index 66698c4..90f65ae 100644
--- a/header_timing_test.go
+++ b/header_timing_test.go
@@ -180,6 +180,19 @@ func benchmarkNormalizeHeaderKey(b *testing.B, src []byte) {
 	})
 }
 
+func BenchmarkVisitHeaderParams(b *testing.B) {
+	var h RequestHeader
+	h.SetBytesKV(strContentType, []byte(`text/plain  ;  foo=bar  ;   param2="dquote is: [\"], ok?" ; version=1; q=0.324  `))
+
+	header := h.ContentType()
+	b.ReportAllocs()
+	b.ResetTimer()
+
+	for n := 0; n < b.N; n++ {
+		VisitHeaderParams(header, func(key, value []byte) bool { return true })
+	}
+}
+
 func BenchmarkRemoveNewLines(b *testing.B) {
 	type testcase struct {
 		value         string
diff --git a/http_test.go b/http_test.go
index 7b526cb..098990a 100644
--- a/http_test.go
+++ b/http_test.go
@@ -2214,7 +2214,7 @@ func testRequestSuccess(t *testing.T, method, requestURI, host, userAgent, body,
 	if string(req1.Header.Method()) != expectedMethod {
 		t.Fatalf("Unexpected method: %q. Expected %q", req1.Header.Method(), expectedMethod)
 	}
-	if len(requestURI) == 0 {
+	if requestURI == "" {
 		requestURI = "/"
 	}
 	if string(req1.Header.RequestURI()) != requestURI {
@@ -2467,7 +2467,7 @@ func testRequestPostArgsError(t *testing.T, req *Request, s string) {
 		t.Fatalf("Unexpected error when reading %q: %v", s, err)
 	}
 	ss := req.PostArgs().String()
-	if len(ss) != 0 {
+	if ss != "" {
 		t.Fatalf("unexpected post args: %q. Expecting empty post args", ss)
 	}
 }