diff --git a/pkg/sessionctx/variable/session.go b/pkg/sessionctx/variable/session.go index 9d47eb8cb13e1..7a12e5d9d7dbf 100644 --- a/pkg/sessionctx/variable/session.go +++ b/pkg/sessionctx/variable/session.go @@ -1848,7 +1848,7 @@ func (p *PlanCacheParamList) String() string { p.forNonPrepCache { // hide non-prep parameter values by default return "" } - return " [arguments: " + types.DatumsToStrNoErr(p.paramValues) + "]" + return " [arguments: " + types.DatumsToStrNoErrSmart(p.paramValues) + "]" } // Append appends a parameter value to the PlanCacheParams. diff --git a/pkg/types/datum.go b/pkg/types/datum.go index 78ebe0d62f346..c765f1bbd228b 100644 --- a/pkg/types/datum.go +++ b/pkg/types/datum.go @@ -24,6 +24,7 @@ import ( "strings" "sync" "time" + "unicode" "unicode/utf8" "unsafe" @@ -2265,8 +2266,34 @@ func (ds *datumsSorter) Swap(i, j int) { var strBuilderPool = sync.Pool{New: func() interface{} { return &strings.Builder{} }} +// Check if a string is considered printable +// +// Checks +// 1. Must be valid UTF-8 +// 2. Must not contain control characters like NUL (0x0) and backspace (0x8) +func isPrintable(s string) bool { + if !utf8.ValidString(s) { + return false + } + for _, r := range s { + if unicode.IsControl(r) { + return false + } + } + return true +} + // DatumsToString converts several datums to formatted string. func DatumsToString(datums []Datum, handleSpecialValue bool) (string, error) { + return datumsToString(datums, handleSpecialValue, false) +} + +// DatumsToStringSmart is like DatumsToString, but with smart detection of non-printable data +func DatumsToStringSmart(datums []Datum, handleSpecialValue bool) (string, error) { + return datumsToString(datums, handleSpecialValue, true) +} + +func datumsToString(datums []Datum, handleSpecialValue bool, binaryAsHex bool) (string, error) { n := len(datums) builder := strBuilderPool.Get().(*strings.Builder) defer func() { @@ -2304,9 +2331,14 @@ func DatumsToString(datums []Datum, handleSpecialValue bool) (string, error) { str = str[:logDatumLen] } if datum.Kind() == KindString { - builder.WriteString(`"`) - builder.WriteString(str) - builder.WriteString(`"`) + if !binaryAsHex || isPrintable(str) { + builder.WriteString(`"`) + builder.WriteString(str) + builder.WriteString(`"`) + } else { + // Print as hex-literal instead + fmt.Fprintf(builder, "0x%X", str) + } } else { builder.WriteString(str) } @@ -2330,6 +2362,15 @@ func DatumsToStrNoErr(datums []Datum) string { return str } +// DatumsToStrNoErrSmart converts some datums to a formatted string. +// If an error occurs, it will print a log instead of returning an error. +// It also enables detection of non-pritable arguments +func DatumsToStrNoErrSmart(datums []Datum) string { + str, err := DatumsToStringSmart(datums, true) + terror.Log(errors.Trace(err)) + return str +} + // CloneRow deep copies a Datum slice. func CloneRow(dr []Datum) []Datum { c := make([]Datum, len(dr)) diff --git a/pkg/types/datum_test.go b/pkg/types/datum_test.go index 88b231194b15d..d7fed625ccd61 100644 --- a/pkg/types/datum_test.go +++ b/pkg/types/datum_test.go @@ -792,3 +792,38 @@ func BenchmarkDatumsToStringLongStr(b *testing.B) { } } } + +func TestIsPrintable(t *testing.T) { + testcases := []struct { + input string + valid bool + }{ + {string([]byte{0x61, 0x62, 0x63}), true}, // abc + {string([]byte{0x61, 0x0, 0x62, 0x63}), false}, // abc + {string([]byte{0x61, 0x62, 0x63, 0xc3, 0xa9}), true}, // abcé + {string([]byte{0x61, 0x62, 0x63, 0xc3}), false}, // abc + } + + for _, tc := range testcases { + r := isPrintable(tc.input) + require.Equal(t, tc.valid, r, "%v (0x%X) expected printable: %v (but got %v)", + tc.input, tc.input, tc.valid, r) + } +} + +func BenchmarkIsPrintable(b *testing.B) { + inputs := []string{ + "abc", + "abcé", + string([]byte{0x61, 0x0, 0x62, 0x63}), // broken: contains NUL + string([]byte{0x61, 0x62, 0x63, 0xc3, 0xa9}), // broken: contains half char + strings.Repeat("abc", 1000), // longer string + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + for _, input := range inputs { + isPrintable(input) + } + } +}