diff --git a/pkg/agent/endpoint/router/build.go b/pkg/agent/endpoint/router/build.go index a2ec9f3a8..f250eecf6 100644 --- a/pkg/agent/endpoint/router/build.go +++ b/pkg/agent/endpoint/router/build.go @@ -54,7 +54,7 @@ func (b *Builder[T]) Add(t *Template, m Mask, v T) error { idx := b.n b.n++ - if ambiguousTemplate(t) { + if t.Ambiguous() { b.ambiguous = append(b.ambiguous, t.raw) } @@ -125,24 +125,6 @@ func singleRun[T any](e *buildEdge[T]) bool { return true } -// ambiguousTemplate reports whether a template's own shape can force a search, -// independent of what other templates put in the tree. -func ambiguousTemplate(t *Template) bool { - for i, e := range t.elements { - if e.kind == kindLiteral || e.kind == kindUUID { - continue - } - if i == len(t.elements)-1 { - continue // terminal: only the greedy run can reach the end - } - next := t.elements[i+1] - if next.kind != kindLiteral || e.kind.charset(next.lit[0]) { - return true - } - } - return false -} - func insertLiteral[T any](n *buildNode[T], lit string, idx uint32) *buildNode[T] { for len(lit) > 0 { e := literalEdge(n, lit[0]) diff --git a/pkg/agent/endpoint/router/template.go b/pkg/agent/endpoint/router/template.go index 97a5d9f14..a065f8390 100644 --- a/pkg/agent/endpoint/router/template.go +++ b/pkg/agent/endpoint/router/template.go @@ -16,6 +16,7 @@ package router import ( "fmt" + "strconv" "strings" "unicode/utf8" ) @@ -67,6 +68,46 @@ type Template struct { // String returns the template as declared. func (t *Template) String() string { return t.raw } +// Canonical returns a key identifying what the template matches: two templates +// with the same canonical form accept exactly the same paths and differ only in +// param names. Literals are length-prefixed so that a literal brace cannot forge +// a convertor - "/x/{:str}" is literal text, and must not collide with "/x/{a}". +func (t *Template) Canonical() string { + var b strings.Builder + for _, e := range t.elements { + if e.kind == kindLiteral { + b.WriteByte('L') + b.WriteString(strconv.Itoa(len(e.lit))) + b.WriteByte(':') + b.WriteString(e.lit) + continue + } + b.WriteByte('K') + b.WriteString(strconv.Itoa(int(e.kind))) + } + return b.String() +} + +// Ambiguous reports whether the template's own shape can force the matcher to +// backtrack - a param a following literal can extend, adjacent params, or a +// non-final path convertor - independent of what other templates put in the +// tree. +func (t *Template) Ambiguous() bool { + for i, e := range t.elements { + if e.kind == kindLiteral || e.kind == kindUUID { + continue + } + if i == len(t.elements)-1 { + continue // terminal: only the greedy run can reach the end + } + next := t.elements[i+1] + if next.kind != kindLiteral || e.kind.charset(next.lit[0]) { + return true + } + } + return false +} + // ParseTemplate parses a starlette path template. Custom convertors are // rejected: only the five built-ins may travel over the wire. func ParseTemplate(path string) (*Template, error) {