mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
Rework Replacer loop to handle escaped braces (#3121)
Fixes #3116 * Rework Replacer loop to ignore escaped braces * Add benchmark tests for replacer * Optimise handling of escaped braces * Handle escaped closing braces * Remove additional check for closing brace This commit removes the additional check for input in which the closing brace appears before the opening brace. This check has been removed for performance reasons as it is deemed an unlikely edge case. * Check for escaped closing braces in placeholder name
This commit is contained in:
parent
ca6e54bbb8
commit
36a6c7daf0
2 changed files with 127 additions and 1 deletions
15
replacer.go
15
replacer.go
|
@ -125,6 +125,14 @@ func (r *Replacer) replace(input, empty string,
|
||||||
// iterate the input to find each placeholder
|
// iterate the input to find each placeholder
|
||||||
var lastWriteCursor int
|
var lastWriteCursor int
|
||||||
for i := 0; i < len(input); i++ {
|
for i := 0; i < len(input); i++ {
|
||||||
|
|
||||||
|
// check for escaped braces
|
||||||
|
if i > 0 && input[i-1] == phEscape && (input[i] == phClose || input[i] == phOpen) {
|
||||||
|
sb.WriteString(input[lastWriteCursor : i-1])
|
||||||
|
lastWriteCursor = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if input[i] != phOpen {
|
if input[i] != phOpen {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -135,6 +143,11 @@ func (r *Replacer) replace(input, empty string,
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if necessary look for the first closing brace that is not escaped
|
||||||
|
for end > 0 && end < len(input)-1 && input[end-1] == phEscape {
|
||||||
|
end = strings.Index(input[end+1:], string(phClose)) + end + 1
|
||||||
|
}
|
||||||
|
|
||||||
// write the substring from the last cursor to this point
|
// write the substring from the last cursor to this point
|
||||||
sb.WriteString(input[lastWriteCursor:i])
|
sb.WriteString(input[lastWriteCursor:i])
|
||||||
|
|
||||||
|
@ -237,4 +250,4 @@ var nowFunc = time.Now
|
||||||
// ReplacerCtxKey is the context key for a replacer.
|
// ReplacerCtxKey is the context key for a replacer.
|
||||||
const ReplacerCtxKey CtxKey = "replacer"
|
const ReplacerCtxKey CtxKey = "replacer"
|
||||||
|
|
||||||
const phOpen, phClose = '{', '}'
|
const phOpen, phClose, phEscape = '{', '}', '\\'
|
||||||
|
|
113
replacer_test.go
113
replacer_test.go
|
@ -35,30 +35,86 @@ func TestReplacer(t *testing.T) {
|
||||||
input: "{",
|
input: "{",
|
||||||
expect: "{",
|
expect: "{",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `\{`,
|
||||||
|
expect: `{`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: "foo{",
|
input: "foo{",
|
||||||
expect: "foo{",
|
expect: "foo{",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `foo\{`,
|
||||||
|
expect: `foo{`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: "foo{bar",
|
input: "foo{bar",
|
||||||
expect: "foo{bar",
|
expect: "foo{bar",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `foo\{bar`,
|
||||||
|
expect: `foo{bar`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: "foo{bar}",
|
input: "foo{bar}",
|
||||||
expect: "foo",
|
expect: "foo",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `foo\{bar\}`,
|
||||||
|
expect: `foo{bar}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: "}",
|
input: "}",
|
||||||
expect: "}",
|
expect: "}",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `\}`,
|
||||||
|
expect: `\}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: "{}",
|
input: "{}",
|
||||||
expect: "",
|
expect: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `\{\}`,
|
||||||
|
expect: `{}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: `{"json": "object"}`,
|
input: `{"json": "object"}`,
|
||||||
expect: "",
|
expect: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `\{"json": "object"}`,
|
||||||
|
expect: `{"json": "object"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `\{"json": "object"\}`,
|
||||||
|
expect: `{"json": "object"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `\{"json": "object{bar}"\}`,
|
||||||
|
expect: `{"json": "object"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `\{"json": \{"nested": "object"\}\}`,
|
||||||
|
expect: `{"json": {"nested": "object"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `\{"json": \{"nested": "{bar}"\}\}`,
|
||||||
|
expect: `{"json": {"nested": ""}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `pre \{"json": \{"nested": "{bar}"\}\}`,
|
||||||
|
expect: `pre {"json": {"nested": ""}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `\{"json": \{"nested": "{bar}"\}\} post`,
|
||||||
|
expect: `{"json": {"nested": ""}} post`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `pre \{"json": \{"nested": "{bar}"\}\} post`,
|
||||||
|
expect: `pre {"json": {"nested": ""}} post`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: `{{`,
|
input: `{{`,
|
||||||
expect: "{{",
|
expect: "{{",
|
||||||
|
@ -67,11 +123,39 @@ func TestReplacer(t *testing.T) {
|
||||||
input: `{{}`,
|
input: `{{}`,
|
||||||
expect: "",
|
expect: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `{"json": "object"\}`,
|
||||||
|
expect: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
input: `{unknown}`,
|
input: `{unknown}`,
|
||||||
empty: "-",
|
empty: "-",
|
||||||
expect: "-",
|
expect: "-",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `back\slashes`,
|
||||||
|
expect: `back\slashes`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `double back\\slashes`,
|
||||||
|
expect: `double back\\slashes`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `placeholder {with \{ brace} in name`,
|
||||||
|
expect: `placeholder in name`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `placeholder {with \} brace} in name`,
|
||||||
|
expect: `placeholder in name`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `placeholder {with \} \} braces} in name`,
|
||||||
|
expect: `placeholder in name`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `\{'group':'default','max_age':3600,'endpoints':[\{'url':'https://some.domain.local/a/d/g'\}],'include_subdomains':true\}`,
|
||||||
|
expect: `{'group':'default','max_age':3600,'endpoints':[{'url':'https://some.domain.local/a/d/g'}],'include_subdomains':true}`,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
actual := rep.ReplaceAll(tc.input, tc.empty)
|
actual := rep.ReplaceAll(tc.input, tc.empty)
|
||||||
if actual != tc.expect {
|
if actual != tc.expect {
|
||||||
|
@ -81,6 +165,35 @@ func TestReplacer(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkReplacer(b *testing.B) {
|
||||||
|
type testCase struct {
|
||||||
|
name, input, empty string
|
||||||
|
}
|
||||||
|
|
||||||
|
rep := testReplacer()
|
||||||
|
|
||||||
|
for _, bm := range []testCase{
|
||||||
|
{
|
||||||
|
name: "no placeholder",
|
||||||
|
input: `simple string`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "placeholder",
|
||||||
|
input: `{"json": "object"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped placeholder",
|
||||||
|
input: `\{"json": \{"nested": "{bar}"\}\}`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
b.Run(bm.name, func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
rep.ReplaceAll(bm.input, bm.empty)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReplacerSet(t *testing.T) {
|
func TestReplacerSet(t *testing.T) {
|
||||||
rep := testReplacer()
|
rep := testReplacer()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue