filter.mx raw
1 package filter
2
3 import (
4 "git.smesh.lol/moxie/pkg/mxutil"
5 "bytes"
6 "crypto/sha256"
7 "sort"
8
9 "crypto/secp256k1"
10 "git.smesh.lol/nostr/pkg/event"
11 "git.smesh.lol/nostr/pkg/ints"
12 "git.smesh.lol/nostr/pkg/kind"
13 "git.smesh.lol/nostr/pkg/tag"
14 "git.smesh.lol/nostr/pkg/text"
15 "git.smesh.lol/nostr/pkg/timestamp"
16 "git.smesh.lol/nostr/pkg/lol/chk"
17 "git.smesh.lol/nostr/pkg/lol/errorf"
18 )
19
20 // The taint flag records that Unmarshal saw undersized hex in ids/authors. It
21 // is a package global behind a self-mutating type: a direct store to a global
22 // is rejected outside init, and callers in other packages (morly's dbengine and
23 // relay/server) must go through the methods, which is why the flag is no longer
24 // an exported variable.
25 type taintState struct{ on bool }
26
27 func (t *taintState) set() { t.on = true }
28 func (t *taintState) clear() { t.on = false }
29 func (t *taintState) get() (ok bool) { return t.on }
30
31 var taint taintState
32
33 // SetTaint, ClearTaint and IsTainted are the cross-package API.
34 func SetTaint() { taint.set() }
35 func ClearTaint() { taint.clear() }
36 func IsTainted() (ok bool) { return taint.get() }
37
38 type F struct {
39 Ids *tag.T `json:"ids,omitempty"`
40 Kinds *kind.S `json:"kinds,omitempty"`
41 Authors *tag.T `json:"authors,omitempty"`
42 Tags *tag.S `json:"-,omitempty"`
43 Since *timestamp.T `json:"since,omitempty"`
44 Until *timestamp.T `json:"until,omitempty"`
45 Search []byte `json:"search,omitempty"`
46 Limit *uint32 `json:"limit,omitempty"`
47 Extra map[string][]byte `json:"-"`
48 }
49
50 func New() (f *F) {
51 return &F{
52 Ids: tag.NewWithCap(10),
53 Kinds: kind.NewWithCap(10),
54 Authors: tag.NewWithCap(10),
55 Tags: tag.NewSWithCap(10),
56 Since: timestamp.New(),
57 Until: timestamp.New(),
58 }
59 }
60
61 var (
62 IDs []byte
63 Kinds []byte
64 Authors []byte
65 Since []byte
66 Until []byte
67 Limit []byte
68 Search []byte
69 )
70
71 func init() {
72 IDs = []byte("ids")
73 Kinds = []byte("kinds")
74 Authors = []byte("authors")
75 Since = []byte("since")
76 Until = []byte("until")
77 Limit = []byte("limit")
78 Search = []byte("search")
79 }
80
81 func (f *F) Sort() {
82 if f.Ids != nil {
83 sort.Sort(f.Ids)
84 }
85 if f.Kinds != nil {
86 sort.Sort(f.Kinds)
87 }
88 if f.Authors != nil {
89 sort.Sort(f.Authors)
90 }
91 if f.Tags != nil {
92 for i, v := range f.Tags.T {
93 if len(v.T) > 2 {
94 // Insertion sort on tag values (v.T[1:]).
95 for ii := 2; ii < len(v.T); ii++ {
96 for jj := ii; jj > 1 && bytes.Compare(v.T[jj], v.T[jj-1]) < 0; jj-- {
97 v.T[jj], v.T[jj-1] = v.T[jj-1], v.T[jj]
98 }
99 }
100 f.Tags.T[i] = v
101 }
102 }
103 sort.Sort(f.Tags)
104 }
105 }
106
107 func (f *F) MatchesIgnoringTimestampConstraints(ev *event.E) (ok bool) {
108 if ev == nil {
109 return false
110 }
111 if f.Ids.Len() > 0 && !f.Ids.Contains(ev.ID) {
112 return false
113 }
114 if f.Kinds.Len() > 0 && !f.Kinds.Contains(ev.Kind) {
115 return false
116 }
117 if f.Authors.Len() > 0 {
118 found := false
119 for _, author := range f.Authors.T {
120 if bytes.Equal(author, ev.Pubkey) {
121 found = true
122 break
123 }
124 }
125 if !found {
126 return false
127 }
128 }
129 if f.Tags.Len() > 0 {
130 for _, v := range f.Tags.T {
131 if v.Len() < 2 {
132 continue
133 }
134 key := v.Key()
135 values := v.T[1:]
136 if !ev.Tags.ContainsAny(key, values) {
137 return false
138 }
139 }
140 }
141 return true
142 }
143
144 func (f *F) Matches(ev *event.E) (match bool) {
145 if !f.MatchesIgnoringTimestampConstraints(ev) {
146 return
147 }
148 if f.Since.Int() != 0 && ev.CreatedAt < f.Since.I64() {
149 return
150 }
151 if f.Until.Int() != 0 && ev.CreatedAt > f.Until.I64() {
152 return
153 }
154 return true
155 }
156
157 func (f *F) Marshal(dst []byte) (b []byte) {
158 var first bool
159 if dst == nil {
160 dst = []byte{:0:256}
161 }
162 f.Sort()
163 b = dst
164 b = b | "{"
165 if f.Ids != nil && f.Ids.Len() > 0 {
166 first = true
167 b = text.JSONKey(b, IDs)
168 b = text.MarshalHexArray(b, f.Ids.T)
169 }
170 if f.Kinds.Len() > 0 {
171 if first {
172 b = b | ","
173 } else {
174 first = true
175 }
176 b = text.JSONKey(b, Kinds)
177 b = f.Kinds.Marshal(b)
178 }
179 if f.Authors.Len() > 0 {
180 if first {
181 b = b | ","
182 } else {
183 first = true
184 }
185 b = text.JSONKey(b, Authors)
186 b = text.MarshalHexArray(b, f.Authors.T)
187 }
188 if f.Tags != nil && f.Tags.Len() > 0 {
189 for _, tg := range f.Tags.T {
190 if tg == nil || tg.Len() < 2 {
191 continue
192 }
193 tKey := tg.T[0]
194 if len(tKey) != 1 ||
195 ((tKey[0] < 'a' || tKey[0] > 'z') && (tKey[0] < 'A' || tKey[0] > 'Z')) {
196 continue
197 }
198 values := tg.T[1:]
199 if len(values) == 0 {
200 continue
201 }
202 if first {
203 b = b | ","
204 } else {
205 first = true
206 }
207 b = b | "\"#" | string([]byte{tKey[0]}) | "\":"
208 b = b | "["
209 for i, value := range values {
210 b = text.AppendQuote(b, value, text.NostrEscape)
211 if i < len(values)-1 {
212 b = b | ","
213 }
214 }
215 b = b | "]"
216 }
217 }
218 if f.Since != nil && f.Since.U64() > 0 {
219 if first {
220 b = b | ","
221 } else {
222 first = true
223 }
224 b = text.JSONKey(b, Since)
225 b = f.Since.Marshal(b)
226 }
227 if f.Until != nil && f.Until.U64() > 0 {
228 if first {
229 b = b | ","
230 } else {
231 first = true
232 }
233 b = text.JSONKey(b, Until)
234 b = f.Until.Marshal(b)
235 }
236 if len(f.Search) > 0 {
237 if first {
238 b = b | ","
239 } else {
240 first = true
241 }
242 b = text.JSONKey(b, Search)
243 b = text.AppendQuote(b, f.Search, text.NostrEscape)
244 }
245 if f.Limit != nil {
246 if first {
247 b = b | ","
248 }
249 b = text.JSONKey(b, Limit)
250 b = ints.New(*f.Limit).Marshal(b)
251 }
252 b = b | "}"
253 return
254 }
255
256 func (f *F) Serialize() (b []byte) { return f.Marshal(nil) }
257
258 const (
259 beforeOpen = iota
260 openParen
261 inKey
262 inKV
263 inVal
264 betweenKV
265 afterClose
266 )
267
268 func (f *F) Unmarshal(b []byte) (r []byte, err error) {
269 r = b
270 var key []byte
271 var state int32
272 // Declared outside the loop: a declaration in the body of a self-mutating
273 // method allocates in the sovereign arena on every iteration.
274 var klen int32
275 var tagKey []byte
276 var ff [][]byte
277
278 var sk bool
279 var untilInt, limitInt, sinceInt *ints.T
280 var limit uint32
281 var txt, val []byte
282 for ; len(r) > 0; r = r[1:] {
283 switch state {
284 case beforeOpen:
285 if r[0] == '{' {
286 state = openParen
287 }
288 case openParen:
289 if r[0] == '"' {
290 state = inKey
291 }
292 case inKey:
293 if r[0] == '"' {
294 state = inKV
295 } else {
296 if key == nil {
297 key = []byte{:0:16}
298 }
299 key = mxutil.Ensure(key, 1)
300 key = push(key, r[0])
301 }
302 case inKV:
303 if r[0] == ':' {
304 state = inVal
305 }
306 case inVal:
307 if len(key) < 1 {
308 err = errorf.E([]byte("filter key zero length: '%s'\n'%s"), b, r)
309 return
310 }
311 // Exact key match, not a first-byte switch with a length bound:
312 // any key sharing a leading byte with a field name was routed into
313 // that field's parser, so a valid but unknown key either failed the
314 // Unmarshal ("arr", "s") or was parsed as the wrong field
315 // ("iXXX" consumed the rest of the filter as ids and dropped a
316 // following "kinds"). Unknown keys belong in Extra, the same as
317 // every key that shares no leading byte.
318 if keyIs(key, IDs) {
319 if ff, r, sk, err = text.UnmarshalHexArray(r, sha256.Size); chk.E(err) {
320 return
321 }
322 if sk {
323 SetTaint()
324 }
325 f.Ids = mkTagT(ff)
326 state = betweenKV
327 } else if keyIs(key, Kinds) {
328 f.Kinds = kind.NewWithCap(0)
329 if r, err = f.Kinds.Unmarshal(r); chk.E(err) {
330 return
331 }
332 state = betweenKV
333 } else if keyIs(key, Authors) {
334 if ff, r, sk, err = text.UnmarshalHexArray(r, secp256k1.PubKeyBytesLen); chk.E(err) {
335 return
336 }
337 if sk {
338 SetTaint()
339 }
340 f.Authors = mkTagT(ff)
341 state = betweenKV
342 } else if keyIs(key, Until) {
343 untilInt = ints.New(0)
344 if r, err = untilInt.Unmarshal(r); chk.E(err) {
345 return
346 }
347 f.Until = timestamp.FromUnix(int64(untilInt.N))
348 state = betweenKV
349 } else if keyIs(key, Limit) {
350 limitInt = ints.New(0)
351 if r, err = limitInt.Unmarshal(r); chk.E(err) {
352 return
353 }
354 limit = uint32(limitInt.N)
355 f.Limit = mkU32(limit)
356 state = betweenKV
357 } else if keyIs(key, Search) {
358 if txt, r, err = text.UnmarshalQuoted(r); chk.E(err) {
359 return
360 }
361 f.Search = txt
362 state = betweenKV
363 } else if keyIs(key, Since) {
364 sinceInt = ints.New(0)
365 if r, err = sinceInt.Unmarshal(r); chk.E(err) {
366 return
367 }
368 f.Since = timestamp.FromUnix(int64(sinceInt.N))
369 state = betweenKV
370 } else if key[0] == '#' {
371 klen = len(key)
372 if klen != 2 {
373 err = errorf.E(
374 []byte("filter tag keys can only be # and one alpha character: '%s'\n%s"),
375 key, b,
376 )
377 return
378 }
379 tagKey = []byte{:1}
380 tagKey[0] = key[1]
381 if ff, r, err = text.UnmarshalStringArray(r); chk.E(err) {
382 return
383 }
384 ff = [][]byte{tagKey} | ff
385 if f.Tags == nil {
386 f.Tags = tag.NewSWithCap(1)
387 }
388 // ff is already [][]byte: build the tag directly. Passing it to
389 // the variadic NewFromBytesSlice(t ...[]byte) wrapped the whole
390 // array as one element and produced a garbage tag.
391 f.Tags.T = push(f.Tags.T, mkTagT(ff))
392 state = betweenKV
393 } else {
394 if val, r, err = skipJSONValue(r); err != nil {
395 goto invalid
396 }
397 if f.Extra == nil {
398 f.Extra = mkByteMap()
399 }
400 f.Extra[string(key)] = val
401 state = betweenKV
402 }
403 key = key[:0]
404 case betweenKV:
405 if len(r) == 0 {
406 return
407 }
408 if r[0] == '}' {
409 state = afterClose
410 } else if r[0] == ',' {
411 state = openParen
412 } else if r[0] == '"' {
413 state = inKey
414 }
415 }
416 if len(r) == 0 {
417 return
418 }
419 if r[0] == '}' {
420 r = r[1:]
421 return
422 }
423 }
424 invalid:
425 err = errorf.E([]byte("invalid key,\n'%s'\n'%s'"), string(b), string(r))
426 return
427 }
428
429 // mkTagT and mkU32 return pointers to values allocated in the CALLER's arena.
430 // A mutating method must not store the address of its own local into the
431 // receiver: the local lands in the receiver's sovereign data arena, which is
432 // reclaimed at the method's batch boundary, so the stored pointer outlives
433 // the value whenever the receiver is a temporary - which is how a parsed
434 // filter ended up with a Limit pointing at unmapped memory. Handing the
435 // value to a free function and returning its address makes the return codec
436 // relocate the pointee into the caller's arena, so it lives as long as the
437 // filter that holds it.
438 func mkTagT(ff [][]byte) (p *tag.T) {
439 return &tag.T{T: ff}
440 }
441
442 func mkU32(v uint32) (p *uint32) {
443 x := v
444 return &x
445 }
446
447 // keyIs reports whether a filter key is exactly the field name want. Unmarshal
448 // dispatches on it: matching only the first byte and a length bound sent every
449 // unknown key that shared a leading byte into a known field's parser.
450 func keyIs(key, want []byte) (ok bool) {
451 if len(key) != len(want) {
452 return false
453 }
454 for i := int32(0); i < len(key); i++ {
455 if key[i] != want[i] {
456 return false
457 }
458 }
459 return true
460 }
461
462 // mkByteMap returns a fresh map allocated in the caller's arena, for the same
463 // reason as mkTagT: a composite literal written inside a mutating method is
464 // allocated in the receiver's sovereign data arena, which is reclaimed at the
465 // method's batch boundary.
466 func mkByteMap() (m map[string][]byte) {
467 return map[string][]byte{}
468 }
469