blob: e54507145b666f8a9e36173c175df96318a22e8c [file] [log] [blame]
Abhay Kumara2ae5992025-11-10 14:02:24 +00001package runtime
2
3import (
4 "errors"
5 "fmt"
6 "strconv"
7 "strings"
8
9 "github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
10 "google.golang.org/grpc/grpclog"
11)
12
13var (
14 // ErrNotMatch indicates that the given HTTP request path does not match to the pattern.
15 ErrNotMatch = errors.New("not match to the path pattern")
16 // ErrInvalidPattern indicates that the given definition of Pattern is not valid.
17 ErrInvalidPattern = errors.New("invalid pattern")
18)
19
20type MalformedSequenceError string
21
22func (e MalformedSequenceError) Error() string {
23 return "malformed path escape " + strconv.Quote(string(e))
24}
25
26type op struct {
27 code utilities.OpCode
28 operand int
29}
30
31// Pattern is a template pattern of http request paths defined in
32// https://github.com/googleapis/googleapis/blob/master/google/api/http.proto
33type Pattern struct {
34 // ops is a list of operations
35 ops []op
36 // pool is a constant pool indexed by the operands or vars.
37 pool []string
38 // vars is a list of variables names to be bound by this pattern
39 vars []string
40 // stacksize is the max depth of the stack
41 stacksize int
42 // tailLen is the length of the fixed-size segments after a deep wildcard
43 tailLen int
44 // verb is the VERB part of the path pattern. It is empty if the pattern does not have VERB part.
45 verb string
46}
47
48// NewPattern returns a new Pattern from the given definition values.
49// "ops" is a sequence of op codes. "pool" is a constant pool.
50// "verb" is the verb part of the pattern. It is empty if the pattern does not have the part.
51// "version" must be 1 for now.
52// It returns an error if the given definition is invalid.
53func NewPattern(version int, ops []int, pool []string, verb string) (Pattern, error) {
54 if version != 1 {
55 grpclog.Errorf("unsupported version: %d", version)
56 return Pattern{}, ErrInvalidPattern
57 }
58
59 l := len(ops)
60 if l%2 != 0 {
61 grpclog.Errorf("odd number of ops codes: %d", l)
62 return Pattern{}, ErrInvalidPattern
63 }
64
65 var (
66 typedOps []op
67 stack, maxstack int
68 tailLen int
69 pushMSeen bool
70 vars []string
71 )
72 for i := 0; i < l; i += 2 {
73 op := op{code: utilities.OpCode(ops[i]), operand: ops[i+1]}
74 switch op.code {
75 case utilities.OpNop:
76 continue
77 case utilities.OpPush:
78 if pushMSeen {
79 tailLen++
80 }
81 stack++
82 case utilities.OpPushM:
83 if pushMSeen {
84 grpclog.Error("pushM appears twice")
85 return Pattern{}, ErrInvalidPattern
86 }
87 pushMSeen = true
88 stack++
89 case utilities.OpLitPush:
90 if op.operand < 0 || len(pool) <= op.operand {
91 grpclog.Errorf("negative literal index: %d", op.operand)
92 return Pattern{}, ErrInvalidPattern
93 }
94 if pushMSeen {
95 tailLen++
96 }
97 stack++
98 case utilities.OpConcatN:
99 if op.operand <= 0 {
100 grpclog.Errorf("negative concat size: %d", op.operand)
101 return Pattern{}, ErrInvalidPattern
102 }
103 stack -= op.operand
104 if stack < 0 {
105 grpclog.Error("stack underflow")
106 return Pattern{}, ErrInvalidPattern
107 }
108 stack++
109 case utilities.OpCapture:
110 if op.operand < 0 || len(pool) <= op.operand {
111 grpclog.Errorf("variable name index out of bound: %d", op.operand)
112 return Pattern{}, ErrInvalidPattern
113 }
114 v := pool[op.operand]
115 op.operand = len(vars)
116 vars = append(vars, v)
117 stack--
118 if stack < 0 {
119 grpclog.Error("stack underflow")
120 return Pattern{}, ErrInvalidPattern
121 }
122 default:
123 grpclog.Errorf("invalid opcode: %d", op.code)
124 return Pattern{}, ErrInvalidPattern
125 }
126
127 if maxstack < stack {
128 maxstack = stack
129 }
130 typedOps = append(typedOps, op)
131 }
132 return Pattern{
133 ops: typedOps,
134 pool: pool,
135 vars: vars,
136 stacksize: maxstack,
137 tailLen: tailLen,
138 verb: verb,
139 }, nil
140}
141
142// MustPattern is a helper function which makes it easier to call NewPattern in variable initialization.
143func MustPattern(p Pattern, err error) Pattern {
144 if err != nil {
145 grpclog.Fatalf("Pattern initialization failed: %v", err)
146 }
147 return p
148}
149
150// MatchAndEscape examines components to determine if they match to a Pattern.
151// MatchAndEscape will return an error if no Patterns matched or if a pattern
152// matched but contained malformed escape sequences. If successful, the function
153// returns a mapping from field paths to their captured values.
154func (p Pattern) MatchAndEscape(components []string, verb string, unescapingMode UnescapingMode) (map[string]string, error) {
155 if p.verb != verb {
156 if p.verb != "" {
157 return nil, ErrNotMatch
158 }
159 if len(components) == 0 {
160 components = []string{":" + verb}
161 } else {
162 components = append([]string{}, components...)
163 components[len(components)-1] += ":" + verb
164 }
165 }
166
167 var pos int
168 stack := make([]string, 0, p.stacksize)
169 captured := make([]string, len(p.vars))
170 l := len(components)
171 for _, op := range p.ops {
172 var err error
173
174 switch op.code {
175 case utilities.OpNop:
176 continue
177 case utilities.OpPush, utilities.OpLitPush:
178 if pos >= l {
179 return nil, ErrNotMatch
180 }
181 c := components[pos]
182 if op.code == utilities.OpLitPush {
183 if lit := p.pool[op.operand]; c != lit {
184 return nil, ErrNotMatch
185 }
186 } else if op.code == utilities.OpPush {
187 if c, err = unescape(c, unescapingMode, false); err != nil {
188 return nil, err
189 }
190 }
191 stack = append(stack, c)
192 pos++
193 case utilities.OpPushM:
194 end := len(components)
195 if end < pos+p.tailLen {
196 return nil, ErrNotMatch
197 }
198 end -= p.tailLen
199 c := strings.Join(components[pos:end], "/")
200 if c, err = unescape(c, unescapingMode, true); err != nil {
201 return nil, err
202 }
203 stack = append(stack, c)
204 pos = end
205 case utilities.OpConcatN:
206 n := op.operand
207 l := len(stack) - n
208 stack = append(stack[:l], strings.Join(stack[l:], "/"))
209 case utilities.OpCapture:
210 n := len(stack) - 1
211 captured[op.operand] = stack[n]
212 stack = stack[:n]
213 }
214 }
215 if pos < l {
216 return nil, ErrNotMatch
217 }
218 bindings := make(map[string]string)
219 for i, val := range captured {
220 bindings[p.vars[i]] = val
221 }
222 return bindings, nil
223}
224
225// MatchAndEscape examines components to determine if they match to a Pattern.
226// It will never perform per-component unescaping (see: UnescapingModeLegacy).
227// MatchAndEscape will return an error if no Patterns matched. If successful,
228// the function returns a mapping from field paths to their captured values.
229//
230// Deprecated: Use MatchAndEscape.
231func (p Pattern) Match(components []string, verb string) (map[string]string, error) {
232 return p.MatchAndEscape(components, verb, UnescapingModeDefault)
233}
234
235// Verb returns the verb part of the Pattern.
236func (p Pattern) Verb() string { return p.verb }
237
238func (p Pattern) String() string {
239 var stack []string
240 for _, op := range p.ops {
241 switch op.code {
242 case utilities.OpNop:
243 continue
244 case utilities.OpPush:
245 stack = append(stack, "*")
246 case utilities.OpLitPush:
247 stack = append(stack, p.pool[op.operand])
248 case utilities.OpPushM:
249 stack = append(stack, "**")
250 case utilities.OpConcatN:
251 n := op.operand
252 l := len(stack) - n
253 stack = append(stack[:l], strings.Join(stack[l:], "/"))
254 case utilities.OpCapture:
255 n := len(stack) - 1
256 stack[n] = fmt.Sprintf("{%s=%s}", p.vars[op.operand], stack[n])
257 }
258 }
259 segs := strings.Join(stack, "/")
260 if p.verb != "" {
261 return fmt.Sprintf("/%s:%s", segs, p.verb)
262 }
263 return "/" + segs
264}
265
266/*
267 * The following code is adopted and modified from Go's standard library
268 * and carries the attached license.
269 *
270 * Copyright 2009 The Go Authors. All rights reserved.
271 * Use of this source code is governed by a BSD-style
272 * license that can be found in the LICENSE file.
273 */
274
275// ishex returns whether or not the given byte is a valid hex character
276func ishex(c byte) bool {
277 switch {
278 case '0' <= c && c <= '9':
279 return true
280 case 'a' <= c && c <= 'f':
281 return true
282 case 'A' <= c && c <= 'F':
283 return true
284 }
285 return false
286}
287
288func isRFC6570Reserved(c byte) bool {
289 switch c {
290 case '!', '#', '$', '&', '\'', '(', ')', '*',
291 '+', ',', '/', ':', ';', '=', '?', '@', '[', ']':
292 return true
293 default:
294 return false
295 }
296}
297
298// unhex converts a hex point to the bit representation
299func unhex(c byte) byte {
300 switch {
301 case '0' <= c && c <= '9':
302 return c - '0'
303 case 'a' <= c && c <= 'f':
304 return c - 'a' + 10
305 case 'A' <= c && c <= 'F':
306 return c - 'A' + 10
307 }
308 return 0
309}
310
311// shouldUnescapeWithMode returns true if the character is escapable with the
312// given mode
313func shouldUnescapeWithMode(c byte, mode UnescapingMode) bool {
314 switch mode {
315 case UnescapingModeAllExceptReserved:
316 if isRFC6570Reserved(c) {
317 return false
318 }
319 case UnescapingModeAllExceptSlash:
320 if c == '/' {
321 return false
322 }
323 case UnescapingModeAllCharacters:
324 return true
325 }
326 return true
327}
328
329// unescape unescapes a path string using the provided mode
330func unescape(s string, mode UnescapingMode, multisegment bool) (string, error) {
331 // TODO(v3): remove UnescapingModeLegacy
332 if mode == UnescapingModeLegacy {
333 return s, nil
334 }
335
336 if !multisegment {
337 mode = UnescapingModeAllCharacters
338 }
339
340 // Count %, check that they're well-formed.
341 n := 0
342 for i := 0; i < len(s); {
343 if s[i] == '%' {
344 n++
345 if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
346 s = s[i:]
347 if len(s) > 3 {
348 s = s[:3]
349 }
350
351 return "", MalformedSequenceError(s)
352 }
353 i += 3
354 } else {
355 i++
356 }
357 }
358
359 if n == 0 {
360 return s, nil
361 }
362
363 var t strings.Builder
364 t.Grow(len(s))
365 for i := 0; i < len(s); i++ {
366 switch s[i] {
367 case '%':
368 c := unhex(s[i+1])<<4 | unhex(s[i+2])
369 if shouldUnescapeWithMode(c, mode) {
370 t.WriteByte(c)
371 i += 2
372 continue
373 }
374 fallthrough
375 default:
376 t.WriteByte(s[i])
377 }
378 }
379
380 return t.String(), nil
381}