package filter import ( "git.smesh.lol/moxie/pkg/mxutil" "bytes" "crypto/sha256" "sort" "crypto/secp256k1" "git.smesh.lol/nostr/pkg/event" "git.smesh.lol/nostr/pkg/ints" "git.smesh.lol/nostr/pkg/kind" "git.smesh.lol/nostr/pkg/tag" "git.smesh.lol/nostr/pkg/text" "git.smesh.lol/nostr/pkg/timestamp" "git.smesh.lol/nostr/pkg/lol/chk" "git.smesh.lol/nostr/pkg/lol/errorf" ) // The taint flag records that Unmarshal saw undersized hex in ids/authors. It // is a package global behind a self-mutating type: a direct store to a global // is rejected outside init, and callers in other packages (morly's dbengine and // relay/server) must go through the methods, which is why the flag is no longer // an exported variable. type taintState struct{ on bool } func (t *taintState) set() { t.on = true } func (t *taintState) clear() { t.on = false } func (t *taintState) get() (ok bool) { return t.on } var taint taintState // SetTaint, ClearTaint and IsTainted are the cross-package API. func SetTaint() { taint.set() } func ClearTaint() { taint.clear() } func IsTainted() (ok bool) { return taint.get() } type F struct { Ids *tag.T `json:"ids,omitempty"` Kinds *kind.S `json:"kinds,omitempty"` Authors *tag.T `json:"authors,omitempty"` Tags *tag.S `json:"-,omitempty"` Since *timestamp.T `json:"since,omitempty"` Until *timestamp.T `json:"until,omitempty"` Search []byte `json:"search,omitempty"` Limit *uint32 `json:"limit,omitempty"` Extra map[string][]byte `json:"-"` } func New() (f *F) { return &F{ Ids: tag.NewWithCap(10), Kinds: kind.NewWithCap(10), Authors: tag.NewWithCap(10), Tags: tag.NewSWithCap(10), Since: timestamp.New(), Until: timestamp.New(), } } var ( IDs []byte Kinds []byte Authors []byte Since []byte Until []byte Limit []byte Search []byte ) func init() { IDs = []byte("ids") Kinds = []byte("kinds") Authors = []byte("authors") Since = []byte("since") Until = []byte("until") Limit = []byte("limit") Search = []byte("search") } func (f *F) Sort() { if f.Ids != nil { sort.Sort(f.Ids) } if f.Kinds != nil { sort.Sort(f.Kinds) } if f.Authors != nil { sort.Sort(f.Authors) } if f.Tags != nil { for i, v := range f.Tags.T { if len(v.T) > 2 { // Insertion sort on tag values (v.T[1:]). for ii := 2; ii < len(v.T); ii++ { for jj := ii; jj > 1 && bytes.Compare(v.T[jj], v.T[jj-1]) < 0; jj-- { v.T[jj], v.T[jj-1] = v.T[jj-1], v.T[jj] } } f.Tags.T[i] = v } } sort.Sort(f.Tags) } } func (f *F) MatchesIgnoringTimestampConstraints(ev *event.E) (ok bool) { if ev == nil { return false } if f.Ids.Len() > 0 && !f.Ids.Contains(ev.ID) { return false } if f.Kinds.Len() > 0 && !f.Kinds.Contains(ev.Kind) { return false } if f.Authors.Len() > 0 { found := false for _, author := range f.Authors.T { if bytes.Equal(author, ev.Pubkey) { found = true break } } if !found { return false } } if f.Tags.Len() > 0 { for _, v := range f.Tags.T { if v.Len() < 2 { continue } key := v.Key() values := v.T[1:] if !ev.Tags.ContainsAny(key, values) { return false } } } return true } func (f *F) Matches(ev *event.E) (match bool) { if !f.MatchesIgnoringTimestampConstraints(ev) { return } if f.Since.Int() != 0 && ev.CreatedAt < f.Since.I64() { return } if f.Until.Int() != 0 && ev.CreatedAt > f.Until.I64() { return } return true } func (f *F) Marshal(dst []byte) (b []byte) { var first bool if dst == nil { dst = []byte{:0:256} } f.Sort() b = dst b = b | "{" if f.Ids != nil && f.Ids.Len() > 0 { first = true b = text.JSONKey(b, IDs) b = text.MarshalHexArray(b, f.Ids.T) } if f.Kinds.Len() > 0 { if first { b = b | "," } else { first = true } b = text.JSONKey(b, Kinds) b = f.Kinds.Marshal(b) } if f.Authors.Len() > 0 { if first { b = b | "," } else { first = true } b = text.JSONKey(b, Authors) b = text.MarshalHexArray(b, f.Authors.T) } if f.Tags != nil && f.Tags.Len() > 0 { for _, tg := range f.Tags.T { if tg == nil || tg.Len() < 2 { continue } tKey := tg.T[0] if len(tKey) != 1 || ((tKey[0] < 'a' || tKey[0] > 'z') && (tKey[0] < 'A' || tKey[0] > 'Z')) { continue } values := tg.T[1:] if len(values) == 0 { continue } if first { b = b | "," } else { first = true } b = b | "\"#" | string([]byte{tKey[0]}) | "\":" b = b | "[" for i, value := range values { b = text.AppendQuote(b, value, text.NostrEscape) if i < len(values)-1 { b = b | "," } } b = b | "]" } } if f.Since != nil && f.Since.U64() > 0 { if first { b = b | "," } else { first = true } b = text.JSONKey(b, Since) b = f.Since.Marshal(b) } if f.Until != nil && f.Until.U64() > 0 { if first { b = b | "," } else { first = true } b = text.JSONKey(b, Until) b = f.Until.Marshal(b) } if len(f.Search) > 0 { if first { b = b | "," } else { first = true } b = text.JSONKey(b, Search) b = text.AppendQuote(b, f.Search, text.NostrEscape) } if f.Limit != nil { if first { b = b | "," } b = text.JSONKey(b, Limit) b = ints.New(*f.Limit).Marshal(b) } b = b | "}" return } func (f *F) Serialize() (b []byte) { return f.Marshal(nil) } const ( beforeOpen = iota openParen inKey inKV inVal betweenKV afterClose ) func (f *F) Unmarshal(b []byte) (r []byte, err error) { r = b var key []byte var state int32 // Declared outside the loop: a declaration in the body of a self-mutating // method allocates in the sovereign arena on every iteration. var klen int32 var tagKey []byte var ff [][]byte var sk bool var untilInt, limitInt, sinceInt *ints.T var limit uint32 var txt, val []byte for ; len(r) > 0; r = r[1:] { switch state { case beforeOpen: if r[0] == '{' { state = openParen } case openParen: if r[0] == '"' { state = inKey } case inKey: if r[0] == '"' { state = inKV } else { if key == nil { key = []byte{:0:16} } key = mxutil.Ensure(key, 1) key = push(key, r[0]) } case inKV: if r[0] == ':' { state = inVal } case inVal: if len(key) < 1 { err = errorf.E([]byte("filter key zero length: '%s'\n'%s"), b, r) return } // Exact key match, not a first-byte switch with a length bound: // any key sharing a leading byte with a field name was routed into // that field's parser, so a valid but unknown key either failed the // Unmarshal ("arr", "s") or was parsed as the wrong field // ("iXXX" consumed the rest of the filter as ids and dropped a // following "kinds"). Unknown keys belong in Extra, the same as // every key that shares no leading byte. if keyIs(key, IDs) { if ff, r, sk, err = text.UnmarshalHexArray(r, sha256.Size); chk.E(err) { return } if sk { SetTaint() } f.Ids = mkTagT(ff) state = betweenKV } else if keyIs(key, Kinds) { f.Kinds = kind.NewWithCap(0) if r, err = f.Kinds.Unmarshal(r); chk.E(err) { return } state = betweenKV } else if keyIs(key, Authors) { if ff, r, sk, err = text.UnmarshalHexArray(r, secp256k1.PubKeyBytesLen); chk.E(err) { return } if sk { SetTaint() } f.Authors = mkTagT(ff) state = betweenKV } else if keyIs(key, Until) { untilInt = ints.New(0) if r, err = untilInt.Unmarshal(r); chk.E(err) { return } f.Until = timestamp.FromUnix(int64(untilInt.N)) state = betweenKV } else if keyIs(key, Limit) { limitInt = ints.New(0) if r, err = limitInt.Unmarshal(r); chk.E(err) { return } limit = uint32(limitInt.N) f.Limit = mkU32(limit) state = betweenKV } else if keyIs(key, Search) { if txt, r, err = text.UnmarshalQuoted(r); chk.E(err) { return } f.Search = txt state = betweenKV } else if keyIs(key, Since) { sinceInt = ints.New(0) if r, err = sinceInt.Unmarshal(r); chk.E(err) { return } f.Since = timestamp.FromUnix(int64(sinceInt.N)) state = betweenKV } else if key[0] == '#' { klen = len(key) if klen != 2 { err = errorf.E( []byte("filter tag keys can only be # and one alpha character: '%s'\n%s"), key, b, ) return } tagKey = []byte{:1} tagKey[0] = key[1] if ff, r, err = text.UnmarshalStringArray(r); chk.E(err) { return } ff = [][]byte{tagKey} | ff if f.Tags == nil { f.Tags = tag.NewSWithCap(1) } // ff is already [][]byte: build the tag directly. Passing it to // the variadic NewFromBytesSlice(t ...[]byte) wrapped the whole // array as one element and produced a garbage tag. f.Tags.T = push(f.Tags.T, mkTagT(ff)) state = betweenKV } else { if val, r, err = skipJSONValue(r); err != nil { goto invalid } if f.Extra == nil { f.Extra = mkByteMap() } f.Extra[string(key)] = val state = betweenKV } key = key[:0] case betweenKV: if len(r) == 0 { return } if r[0] == '}' { state = afterClose } else if r[0] == ',' { state = openParen } else if r[0] == '"' { state = inKey } } if len(r) == 0 { return } if r[0] == '}' { r = r[1:] return } } invalid: err = errorf.E([]byte("invalid key,\n'%s'\n'%s'"), string(b), string(r)) return } // mkTagT and mkU32 return pointers to values allocated in the CALLER's arena. // A mutating method must not store the address of its own local into the // receiver: the local lands in the receiver's sovereign data arena, which is // reclaimed at the method's batch boundary, so the stored pointer outlives // the value whenever the receiver is a temporary - which is how a parsed // filter ended up with a Limit pointing at unmapped memory. Handing the // value to a free function and returning its address makes the return codec // relocate the pointee into the caller's arena, so it lives as long as the // filter that holds it. func mkTagT(ff [][]byte) (p *tag.T) { return &tag.T{T: ff} } func mkU32(v uint32) (p *uint32) { x := v return &x } // keyIs reports whether a filter key is exactly the field name want. Unmarshal // dispatches on it: matching only the first byte and a length bound sent every // unknown key that shared a leading byte into a known field's parser. func keyIs(key, want []byte) (ok bool) { if len(key) != len(want) { return false } for i := int32(0); i < len(key); i++ { if key[i] != want[i] { return false } } return true } // mkByteMap returns a fresh map allocated in the caller's arena, for the same // reason as mkTagT: a composite literal written inside a mutating method is // allocated in the receiver's sovereign data arena, which is reclaimed at the // method's batch boundary. func mkByteMap() (m map[string][]byte) { return map[string][]byte{} }