diff --git a/models/points.go b/models/points.go index 6d083f7e7d..24e4637734 100644 --- a/models/points.go +++ b/models/points.go @@ -1095,34 +1095,17 @@ func unescapeTag(in []byte) []byte { return in } +// escapeStringFieldReplacer replaces double quotes and backslashes +// with the same character preceded by a backslash. +// As of Go 1.7 this benchmarked better in allocations and CPU time +// compared to iterating through a string byte-by-byte and appending to a new byte slice, +// calling strings.Replace twice, and better than (*Regex).ReplaceAllString. +var escapeStringFieldReplacer = strings.NewReplacer(`"`, `\"`, `\`, `\\`) + // EscapeStringField returns a copy of in with any double quotes or // backslashes with escaped values func EscapeStringField(in string) string { - var out []byte - i := 0 - for { - if i >= len(in) { - break - } - // escape double-quotes - if in[i] == '\\' { - out = append(out, '\\') - out = append(out, '\\') - i++ - continue - } - // escape double-quotes - if in[i] == '"' { - out = append(out, '\\') - out = append(out, '"') - i++ - continue - } - out = append(out, in[i]) - i++ - - } - return string(out) + return escapeStringFieldReplacer.Replace(in) } // unescapeStringField returns a copy of in with any escaped double-quotes diff --git a/models/points_test.go b/models/points_test.go index 0652964696..d1db8a4ee4 100644 --- a/models/points_test.go +++ b/models/points_test.go @@ -26,6 +26,8 @@ var ( } maxFloat64 = strconv.FormatFloat(math.MaxFloat64, 'f', 1, 64) minFloat64 = strconv.FormatFloat(-math.MaxFloat64, 'f', 1, 64) + + sink interface{} ) func TestMarshal(t *testing.T) { @@ -2168,3 +2170,61 @@ func TestPoint_FieldIterator_Delete_Twice(t *testing.T) { t.Fatalf("Delete failed, got %#v, exp %#v", got, exp) } } + +func TestEscapeStringField(t *testing.T) { + cases := []struct { + in string + expOut string + }{ + {in: "abcdefg", expOut: "abcdefg"}, + {in: `one double quote " .`, expOut: `one double quote \" .`}, + {in: `quote " then backslash \ .`, expOut: `quote \" then backslash \\ .`}, + {in: `backslash \ then quote " .`, expOut: `backslash \\ then quote \" .`}, + } + + for _, c := range cases { + // Unescapes as expected. + got := models.EscapeStringField(c.in) + if got != c.expOut { + t.Errorf("unexpected result from EscapeStringField(%s)\ngot [%s]\nexp [%s]\n", c.in, got, c.expOut) + continue + } + + pointLine := fmt.Sprintf(`t s="%s"`, got) + test(t, pointLine, NewTestPoint( + "t", + models.NewTags(nil), + models.Fields{"s": c.in}, + time.Unix(0, 0), + )) + } +} + +func BenchmarkEscapeStringField_Plain(b *testing.B) { + s := "nothing special" + for i := 0; i < b.N; i++ { + sink = models.EscapeStringField(s) + } +} + +func BenchmarkEscapeString_Quotes(b *testing.B) { + s := `Hello, "world"` + for i := 0; i < b.N; i++ { + sink = models.EscapeStringField(s) + } +} + +func BenchmarkEscapeString_Backslashes(b *testing.B) { + s := `C:\windows\system32` + for i := 0; i < b.N; i++ { + sink = models.EscapeStringField(s) + } +} + +func BenchmarkEscapeString_QuotesAndBackslashes(b *testing.B) { + s1 := `a quote " then backslash \ .` + s2 := `a backslash \ then quote " .` + for i := 0; i < b.N; i++ { + sink = [...]string{models.EscapeStringField(s1), models.EscapeStringField(s2)} + } +}