Skip to content

Commit

Permalink
Optimize the performance and security of string conversion.
Browse files Browse the repository at this point in the history
  • Loading branch information
edoger committed Apr 7, 2023
1 parent 2e9cec5 commit 3662986
Show file tree
Hide file tree
Showing 2 changed files with 313 additions and 23 deletions.
138 changes: 116 additions & 22 deletions internal/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package internal

import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
)

Expand Down Expand Up @@ -66,7 +68,7 @@ func StandardiseFieldsForJSONEncoder(src map[string]interface{}) map[string]inte
case error:
// The json.Marshal will convert some errors into "{}", we need to call
// the error.Error() method before JSON encoding.
dst[k] = o.Error()
dst[k] = errorToString(o)
default:
dst[k] = v
}
Expand All @@ -78,14 +80,7 @@ func StandardiseFieldsForJSONEncoder(src map[string]interface{}) map[string]inte
func FormatFieldsToText(src map[string]interface{}) string {
texts := make([]string, 0, len(src))
for k, v := range src {
switch o := v.(type) {
case []byte:
texts = append(texts, k+"="+string(o))
case fmt.Stringer:
texts = append(texts, k+"="+o.String())
default:
texts = append(texts, k+"="+fmt.Sprint(v))
}
texts = append(texts, k+"="+ToString(v))
}
// Ensure that the order of log extension fields is consistent.
if len(texts) > 1 {
Expand All @@ -97,25 +92,124 @@ func FormatFieldsToText(src map[string]interface{}) string {
// FormatPairsToFields standardizes the given pairs to fields.
func FormatPairsToFields(pairs []interface{}) map[string]interface{} {
fields := make(map[string]interface{}, len(pairs)/2)
var key string
for i, j := 0, len(pairs); i < j; i += 2 {
switch pair := pairs[i].(type) {
case string:
key = pair
case fmt.Stringer:
key = pair.String()
default:
// We tried converting to a string, but this shouldn't happen, normally, the key
// of a key-value pair should be a string.
key = fmt.Sprint(pairs[i])
}
if i+1 < j {
fields[key] = pairs[i+1]
fields[ToString(pairs[i])] = pairs[i+1]
} else {
// Can't be the last key-value pair?
// We tried setting the value to an empty string, but that shouldn't happen.
fields[key] = ""
fields[ToString(pairs[i])] = ""
}
}
return fields
}

var (
errorType = reflect.TypeOf((*error)(nil)).Elem()
stringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
)

// ToString tries to convert the given variable into a string.
func ToString(value interface{}) string {
if value == nil {
return ""
}
switch v := value.(type) {
case string:
return v
case fmt.Stringer:
return stringerToString(v)
case int:
return strconv.Itoa(v)
case int64:
return strconv.FormatInt(v, 10)
case int32:
return strconv.FormatInt(int64(v), 10)
case int16:
return strconv.FormatInt(int64(v), 10)
case int8:
return strconv.FormatInt(int64(v), 10)
case uint:
return strconv.FormatUint(uint64(v), 10)
case uint64:
return strconv.FormatUint(v, 10)
case uint32:
return strconv.FormatUint(uint64(v), 10)
case uint16:
return strconv.FormatUint(uint64(v), 10)
case uint8:
return strconv.FormatUint(uint64(v), 10)
case bool:
return strconv.FormatBool(v)
case float64:
return strconv.FormatFloat(v, 'g', -1, 64)
case float32:
return strconv.FormatFloat(float64(v), 'g', -1, 32)
case []byte:
return string(v)
case error:
return errorToString(v)
}
// Not a common type? We try to use reflection for fast conversion to string.
rv := reflect.ValueOf(value)
for k := rv.Kind(); k == reflect.Ptr || k == reflect.Interface; k = rv.Kind() {
if rv.IsNil() {
return ""
}
if rv.Type().AssignableTo(errorType) {
return errorToString(rv.Interface().(error))
}
if rv.Type().AssignableTo(stringerType) {
return stringerToString(rv.Interface().(fmt.Stringer))
}
rv = rv.Elem()
}
switch rv.Kind() {
case reflect.String:
return rv.String()
case reflect.Int64, reflect.Int, reflect.Int32, reflect.Int16, reflect.Int8:
return strconv.FormatInt(rv.Int(), 10)
case reflect.Uint64, reflect.Uint, reflect.Uint32, reflect.Uint16, reflect.Uint8:
return strconv.FormatUint(rv.Uint(), 10)
case reflect.Bool:
return strconv.FormatBool(rv.Bool())
case reflect.Float64:
return strconv.FormatFloat(rv.Float(), 'g', -1, 64)
case reflect.Float32:
return strconv.FormatFloat(rv.Float(), 'g', -1, 32)
case reflect.Slice:
if rv.Type().Elem().Kind() == reflect.Uint8 {
return string(rv.Bytes())
}
}
// Ultimately, we can only hope that this returns the desired string.
return fmt.Sprint(value)
}

// Safely convert the given error to a string.
func errorToString(err error) (s string) {
if err == nil {
return
}
defer func() {
if v := recover(); v != nil {
s = "!!PANIC(error.Error)"
}
}()
s = err.Error()
return
}

// Safely convert the given fmt.Stringer to a string.
func stringerToString(sr fmt.Stringer) (s string) {
if sr == nil {
return
}
defer func() {
if v := recover(); v != nil {
s = "!!PANIC(fmt.Stringer.String)"
}
}()
s = sr.String()
return
}
198 changes: 197 additions & 1 deletion internal/fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ type testStringer struct {
v string
}

func (o *testStringer) String() string {
func (o testStringer) String() string {
return o.v
}

Expand Down Expand Up @@ -136,3 +136,199 @@ func TestFormatPairsToFields(t *testing.T) {
}
}
}

func TestToString(t *testing.T) {
s := "s"
n := 1
b := true
f := 1.5
f32 := float32(1.5)
iu64 := uint64(1)

ps := &s
pn := &n
pb := &b
pf := &f
pf32 := &f32
piu64 := &iu64

psr := &testStringer{"s"}
psr2 := &psr

bs := []byte("bs")

nilError := error(nil)
nilSr := fmt.Stringer(nil)

type aliasInt int
type aliasString string
type aliasBool bool
type aliasError error
type aliasStringer fmt.Stringer

ai := aliasInt(1)
as := aliasString("s")
ab := aliasBool(true)
ae := aliasError(errors.New("err"))
asr1 := aliasStringer(&testStringer{"s"})
asr2 := aliasStringer(testStringer{"s"})

nilAe := aliasError(nil)
nilAsr := aliasStringer(nil)

items := []struct {
Given interface{}
Want string
}{
{"s", "s"},
{1, "1"}, // int
{int8(1), "1"},
{int16(1), "1"},
{int32(1), "1"},
{int64(1), "1"},
{uint(1), "1"},
{uint8(1), "1"},
{uint16(1), "1"},
{uint32(1), "1"},
{uint64(1), "1"},
{uint8(1), "1"},
{1.5, "1.5"}, // float64
{float32(1.5), "1.5"},
{[]byte("b"), "b"},
{errors.New("err"), "err"},
{error(nil), ""},
{nilError, ""},
{&nilError, ""},
{fmt.Stringer(nil), ""},
{nilSr, ""},
{&nilSr, ""},
{testStringer{"s"}, "s"},
{&testStringer{"s"}, "s"},
{&psr, "s"},
{psr2, "s"},
{true, "true"},
{ps, "s"}, // *string
{pn, "1"}, // *int
{pb, "true"}, // *bool
{pf, "1.5"}, // *float64
{pf32, "1.5"}, // *float32
{piu64, "1"}, // *uint64
{&ps, "s"}, // **string
{&pn, "1"}, // **int
{&pb, "true"}, // **bool
{&pf, "1.5"}, // **float64
{&pf32, "1.5"}, // **float32
{&piu64, "1"}, // **uint64
{&bs, "bs"}, // *[]byte
{[]string{"s", "s"}, fmt.Sprint([]string{"s", "s"})},
{nil, ""},
{aliasInt(1), "1"},
{aliasString("s"), "s"},
{aliasBool(true), "true"},
{aliasError(nil), ""},
{aliasError(errors.New("err")), "err"},
{aliasStringer(nil), ""},
{aliasStringer(testStringer{"s"}), "s"},
{aliasStringer(&testStringer{"s"}), "s"},
{&ai, "1"},
{&as, "s"},
{&ab, "true"},
{&ae, "err"},
{&asr1, "s"},
{&asr2, "s"},
{&nilAe, ""},
{&nilAsr, ""},
}

for i, item := range items {
if got := ToString(item.Given); got != item.Want {
t.Fatalf("ToString(): [%d] want %q got %q", i, item.Want, got)
}
}
}

type testPanicError struct{}

func (testPanicError) Error() string {
panic("testPanicError")
}

type testPanicStringer struct{}

func (testPanicStringer) String() string {
panic("testPanicStringer")
}

func TestStandardiseFieldsForJSONEncoder_WithPanicError(t *testing.T) {
src := map[string]interface{}{
"err": new(testPanicError),
}
dst := StandardiseFieldsForJSONEncoder(src)

bs, err := json.Marshal(dst)
if err != nil {
t.Fatal(err)
}

want := `{"err":"!!PANIC(error.Error)"}`
got := string(bs)
if want != got {
t.Fatalf("StandardiseFieldsForJSONEncoder(): want %q, got %q", want, got)
}
}

func TestFormatFieldsToText_WithPanicError(t *testing.T) {
want := `err=!!PANIC(error.Error)`
got := FormatFieldsToText(map[string]interface{}{
"err": new(testPanicError),
})
if want != got {
t.Fatalf("FormatFieldsToText(): want %q, got %q", want, got)
}
}

func TestFormatFieldsToText_WithPanicStringer(t *testing.T) {
want := `stringer=!!PANIC(fmt.Stringer.String)`
got := FormatFieldsToText(map[string]interface{}{
"stringer": new(testPanicStringer),
})
if want != got {
t.Fatalf("FormatFieldsToText(): want %q, got %q", want, got)
}
}

func TestFormatPairsToFields_WithPanicError(t *testing.T) {
got := FormatPairsToFields([]interface{}{
new(testPanicError), "test",
})
want := map[string]interface{}{
"!!PANIC(error.Error)": "test",
}
if len(want) != len(got) {
t.Fatalf("FormatPairsToFields(): %v", got)
}
for k, v := range want {
// All value is string.
if got[k].(string) != v.(string) {
t.Fatalf("FormatPairsToFields(): %v", got)
}
}
}

func TestFormatPairsToFields_WithPanicStringer(t *testing.T) {
got := FormatPairsToFields([]interface{}{
new(testPanicStringer), "test",
})
want := map[string]interface{}{
"!!PANIC(fmt.Stringer.String)": "test",
}
if len(want) != len(got) {
t.Fatalf("FormatPairsToFields(): %v", got)
}
for k, v := range want {
// All value is string.
if got[k].(string) != v.(string) {
t.Fatalf("FormatPairsToFields(): %v", got)
}
}
}

0 comments on commit 3662986

Please sign in to comment.