-
Notifications
You must be signed in to change notification settings - Fork 111
Expand file tree
/
Copy pathparser.go
More file actions
325 lines (269 loc) · 8.54 KB
/
Copy pathparser.go
File metadata and controls
325 lines (269 loc) · 8.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
// Package boolpolicy implements a recursive-descent parser for boolean
// expressions of the form "$0 OR ($1 AND $2)", where $N is an index
// reference into a caller-supplied slice of bool values.
//
// Grammar (OR binds less tightly than AND):
//
// expr = or_expr
//
// or_expr = and_expr ( 'OR' and_expr )*
// and_expr = primary ( 'AND' primary )*
// primary = '$' digits | '(' expr ')'
package boolpolicy
import (
"fmt"
"strconv"
"strings"
"unicode"
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
)
const (
// maxPolicyLen is the maximum byte length accepted by Parse.
// Policies longer than 4 KB cannot represent a sensible real-world
// expression and are almost certainly an attack or a bug.
maxPolicyLen = 4 * 1024
// maxParseDepth is the maximum parenthesis nesting depth allowed by the
// recursive-descent parser. Each open parenthesis adds one frame to the
// Go call stack; capping at 64 prevents goroutine stack exhaustion from
// attacker-supplied deeply-nested expressions.
maxParseDepth = 64
// maxParseNodes is the maximum number of AST nodes (RefNode/AndNode/OrNode)
// that Parse will construct for a single expression. The depth cap bounds
// the shape of the tree but not its size: a flat, unparenthesised chain such
// as "$0 OR $1 OR ..." stays at depth 0 while producing one node per token.
// Capping the total node count bounds the memory footprint of the AST — and
// the work any later traversal performs — regardless of expression shape.
// 1024 nodes comfortably covers real policies (hundreds of component
// identities) while staying well under the ~2000-node worst case the
// maxPolicyLen byte limit alone would otherwise permit.
maxParseNodes = 1024
)
// ---------------------------------------------------------------------------
// AST nodes
// ---------------------------------------------------------------------------
// Node is the common interface for all AST nodes produced by Parse.
type Node interface {
// Eval evaluates the node against a slice of resolved boolean values.
// A RefNode with index i returns refs[i]; out-of-range indices return false.
Eval(refs []bool) bool
String() string
}
// RefNode references a single boolean value by its index (e.g. $3).
type RefNode struct {
Index int
}
func (r *RefNode) Eval(refs []bool) bool {
if r.Index < 0 || r.Index >= len(refs) {
return false
}
return refs[r.Index]
}
func (r *RefNode) String() string { return fmt.Sprintf("$%d", r.Index) }
// AndNode represents Left AND Right.
type AndNode struct {
Left, Right Node
}
func (a *AndNode) Eval(refs []bool) bool { return a.Left.Eval(refs) && a.Right.Eval(refs) }
func (a *AndNode) String() string { return fmt.Sprintf("(%s AND %s)", a.Left, a.Right) }
// OrNode represents Left OR Right.
type OrNode struct {
Left, Right Node
}
func (o *OrNode) Eval(refs []bool) bool { return o.Left.Eval(refs) || o.Right.Eval(refs) }
func (o *OrNode) String() string { return fmt.Sprintf("(%s OR %s)", o.Left, o.Right) }
// ---------------------------------------------------------------------------
// Lexer
// ---------------------------------------------------------------------------
type tokenKind int
const (
tokEOF tokenKind = iota
tokRef
tokAnd
tokOr
tokLParen
tokRParen
)
type lexToken struct {
kind tokenKind
index int // populated for tokRef
}
type lexer struct {
runes []rune
pos int
}
func newLexer(s string) *lexer { return &lexer{runes: []rune(s)} }
func (l *lexer) skipSpace() {
for l.pos < len(l.runes) && unicode.IsSpace(l.runes[l.pos]) {
l.pos++
}
}
func (l *lexer) next() (lexToken, error) {
l.skipSpace()
if l.pos >= len(l.runes) {
return lexToken{kind: tokEOF}, nil
}
ch := l.runes[l.pos]
switch {
case ch == '(':
l.pos++
return lexToken{kind: tokLParen}, nil
case ch == ')':
l.pos++
return lexToken{kind: tokRParen}, nil
case ch == '$':
l.pos++ // consume '$'
start := l.pos
for l.pos < len(l.runes) && unicode.IsDigit(l.runes[l.pos]) {
l.pos++
}
if l.pos == start {
return lexToken{}, errors.Errorf("expected digit after '$' at position %d", l.pos)
}
idx, err := strconv.Atoi(string(l.runes[start:l.pos]))
if err != nil {
return lexToken{}, errors.Wrapf(err, "invalid index at position %d", start)
}
return lexToken{kind: tokRef, index: idx}, nil
case unicode.IsLetter(ch):
start := l.pos
for l.pos < len(l.runes) && unicode.IsLetter(l.runes[l.pos]) {
l.pos++
}
word := strings.ToUpper(string(l.runes[start:l.pos]))
switch word {
case "AND":
return lexToken{kind: tokAnd}, nil
case "OR":
return lexToken{kind: tokOr}, nil
default:
return lexToken{}, errors.Errorf("unknown keyword %q at position %d", word, start)
}
default:
return lexToken{}, errors.Errorf("unexpected character %q at position %d", string(ch), l.pos)
}
}
// ---------------------------------------------------------------------------
// Parser
// ---------------------------------------------------------------------------
// parser holds a one-token lookahead over the lexer stream.
type parser struct {
lex *lexer
current lexToken
err error
nodes int // number of AST nodes constructed so far
}
// countNode records the construction of one AST node and returns whether the
// parser is still within the maxParseNodes budget. On overflow it latches an
// error (if one is not already set) so the caller can bail out immediately.
func (p *parser) countNode() bool {
p.nodes++
if p.nodes > maxParseNodes {
if p.err == nil {
p.err = errors.Errorf("policy expression exceeds maximum node count of %d", maxParseNodes)
}
return false
}
return true
}
// Parse parses a boolean expression string and returns the root AST node.
// It returns an error for any lexical or syntactic problems.
//
// Parse enforces three hard limits to prevent resource exhaustion:
// - input longer than maxPolicyLen bytes is rejected immediately.
// - parenthesis nesting deeper than maxParseDepth levels is rejected;
// this bounds the Go call-stack depth of the recursive descent and
// prevents goroutine stack exhaustion from attacker-supplied input.
// - expressions producing more than maxParseNodes AST nodes are rejected;
// this bounds the size of the AST (and the cost of any later traversal)
// independently of its shape, since the depth cap alone does not limit a
// flat chain such as "$0 OR $1 OR ...".
func Parse(input string) (Node, error) {
if len(input) > maxPolicyLen {
return nil, errors.Errorf("policy expression exceeds maximum length of %d bytes (got %d)", maxPolicyLen, len(input))
}
p := &parser{lex: newLexer(input)}
p.advance() // prime the lookahead
if p.err != nil {
return nil, p.err
}
node := p.parseOr(0)
if p.err != nil {
return nil, p.err
}
if p.current.kind != tokEOF {
return nil, errors.New("unexpected token after expression")
}
return node, nil
}
func (p *parser) advance() {
if p.err != nil {
return
}
p.current, p.err = p.lex.next()
}
// parseOr handles: and_expr ( 'OR' and_expr )*
// OR is left-associative and lower precedence than AND.
func (p *parser) parseOr(depth int) Node {
left := p.parseAnd(depth)
for p.err == nil && p.current.kind == tokOr {
p.advance()
right := p.parseAnd(depth)
if !p.countNode() {
return nil
}
left = &OrNode{Left: left, Right: right}
}
return left
}
// parseAnd handles: primary ( 'AND' primary )*
// AND is left-associative and higher precedence than OR.
func (p *parser) parseAnd(depth int) Node {
left := p.parsePrimary(depth)
for p.err == nil && p.current.kind == tokAnd {
p.advance()
right := p.parsePrimary(depth)
if !p.countNode() {
return nil
}
left = &AndNode{Left: left, Right: right}
}
return left
}
// parsePrimary handles: '$' digits | '(' expr ')'
func (p *parser) parsePrimary(depth int) Node {
if p.err != nil {
return nil
}
switch p.current.kind {
case tokRef:
if !p.countNode() {
return nil
}
node := &RefNode{Index: p.current.index}
p.advance()
return node
case tokLParen:
if depth >= maxParseDepth {
p.err = errors.Errorf("policy expression exceeds maximum nesting depth of %d", maxParseDepth)
return nil
}
p.advance() // consume '('
node := p.parseOr(depth + 1)
if p.err != nil {
return nil
}
if p.current.kind != tokRParen {
p.err = errors.Errorf("expected ')' but got token kind %v", p.current.kind)
return nil
}
p.advance() // consume ')'
return node
default:
p.err = errors.Errorf("expected '$N' or '(' at position %d", p.lex.pos)
return nil
}
}