package filter import ( "bytes" "testing" "git.smesh.lol/smesh/pkg/nostr/event" "git.smesh.lol/smesh/pkg/nostr/kind" "git.smesh.lol/smesh/pkg/nostr/tag" "git.smesh.lol/smesh/pkg/nostr/timestamp" ) // tHash returns n copies of fill as a 32-byte hash-shaped value. func tHash(fill byte) (b []byte) { b = []byte{:32} for i := range b { b[i] = fill } return } // tHex renders 32 copies of fill as lowercase hex. func tHex(fill byte) (s []byte) { tbl := []byte("0123456789abcdef") hi := tbl[int32(fill>>4)] lo := tbl[int32(fill&0x0F)] s = []byte{:64} for i := 0; i < 32; i++ { s[i*2] = hi s[i*2+1] = lo } return } func tTag(k, v string) (tt *tag.T) { return tag.NewFromBytesSlice([]byte(k), []byte(v)) } func tEvent(id, pub []byte, k uint16, created int64, tags *tag.S) (ev *event.E) { ev = event.New() ev.ID = id ev.Pubkey = pub ev.Kind = k ev.CreatedAt = created ev.Tags = tags return } func TestNewFilterAndSerialize(t *testing.T) { f := New() if f == nil { t.Fatal("New returned nil") return } if f.Ids == nil || f.Kinds == nil || f.Authors == nil || f.Tags == nil { t.Fatal("New must initialize the list fields") } if f.Since == nil || f.Until == nil { t.Fatal("New must initialize the timestamp fields") } if f.Ids.Len() != 0 || f.Kinds.Len() != 0 || f.Authors.Len() != 0 || f.Tags.Len() != 0 { t.Fatal("New lists must start empty") } if f.Limit != nil || len(f.Search) != 0 || f.Extra != nil { t.Fatal("New leaves Limit, Search and Extra unset") } if string(f.Serialize()) != "{}" { t.Fatalf("empty Serialize = %s", string(f.Serialize())) } if string(f.Marshal(nil)) != "{}" { t.Fatalf("empty Marshal = %s", string(f.Marshal(nil))) } // A zero F has every field absent and must marshal the same way. z := &F{} if string(z.Marshal(nil)) != "{}" { t.Fatalf("zero F Marshal = %s", string(z.Marshal(nil))) } // Sort must tolerate every list being nil. z.Sort() } func TestSortOrdersAllFields(t *testing.T) { f := New() f.Ids = tag.NewFromBytesSlice(tHash(3), tHash(1), tHash(2)) f.Authors = tag.NewFromBytesSlice(tHash(9), tHash(5)) f.Kinds = kind.FromIntSlice([]int32{9, 1, 5}) f.Tags = tag.NewS( tag.NewFromBytesSlice([]byte("t"), []byte("c"), []byte("a"), []byte("b")), tag.NewFromBytesSlice([]byte("a"), []byte("z")), ) f.Sort() if !bytes.Equal(f.Ids.T[0], tHash(1)) || !bytes.Equal(f.Ids.T[1], tHash(2)) || !bytes.Equal(f.Ids.T[2], tHash(3)) { t.Fatal("Ids not sorted ascending") } if !bytes.Equal(f.Authors.T[0], tHash(5)) || !bytes.Equal(f.Authors.T[1], tHash(9)) { t.Fatal("Authors not sorted ascending") } if f.Kinds.K[0].K != 1 || f.Kinds.K[1].K != 5 || f.Kinds.K[2].K != 9 { t.Fatal("Kinds not sorted ascending") } if !bytes.Equal(f.Tags.T[0].T[0], []byte("a")) || !bytes.Equal(f.Tags.T[1].T[0], []byte("t")) { t.Fatal("Tags not sorted by key") } tv := f.Tags.T[1] if !bytes.Equal(tv.T[0], []byte("t")) { t.Fatal("tag key moved during value sort") } if !bytes.Equal(tv.T[1], []byte("a")) || !bytes.Equal(tv.T[2], []byte("b")) || !bytes.Equal(tv.T[3], []byte("c")) { t.Fatal("tag values not sorted") } } func TestMatchesIgnoringTimestampConstraints(t *testing.T) { ev := tEvent(tHash(1), tHash(2), 1, 100, tag.NewS(tTag("t", "alpha"), tTag("x", "beta"))) nf := New() if nf.MatchesIgnoringTimestampConstraints(nil) { t.Fatal("nil event must not match") } if !nf.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("an empty filter matches any event") } fid := New() fid.Ids = tag.NewFromBytesSlice(tHash(1)) if !fid.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("ids: exact id must match") } fid2 := New() fid2.Ids = tag.NewFromBytesSlice(tHash(7)) if fid2.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("ids: different id must not match") } fk := New() fk.Kinds = kind.FromIntSlice([]int32{1}) if !fk.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("kinds: exact kind must match") } fk2 := New() fk2.Kinds = kind.FromIntSlice([]int32{2}) if fk2.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("kinds: different kind must not match") } fa := New() fa.Authors = tag.NewFromBytesSlice(tHash(2)) if !fa.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("authors: exact author must match") } fa2 := New() fa2.Authors = tag.NewFromBytesSlice(tHash(8)) if fa2.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("authors: different author must not match") } ft := New() ft.Tags = tag.NewS(tTag("t", "alpha")) if !ft.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("tags: exact value must match") } ft2 := New() ft2.Tags = tag.NewS(tTag("t", "gamma")) if ft2.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("tags: different value must not match") } ft3 := New() ft3.Tags = tag.NewS(tTag("z", "alpha")) if ft3.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("tags: different name must not match") } // A filter tag with a key but no value is skipped, not enforced. ft4 := New() ft4.Tags = tag.NewS(tag.NewFromBytesSlice([]byte("t"))) if !ft4.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("one-field filter tag must be skipped") } // With no event tags a tag constraint cannot be satisfied. evNoTags := tEvent(tHash(1), tHash(2), 1, 100, nil) ft5 := New() ft5.Tags = tag.NewS(tTag("t", "alpha")) if ft5.MatchesIgnoringTimestampConstraints(evNoTags) { t.Fatal("tag constraint must fail without event tags") } // Timestamp fields are deliberately ignored here. fstamp := New() fstamp.Since = timestamp.FromUnix(1000) fstamp.Until = timestamp.FromUnix(2000) if !fstamp.MatchesIgnoringTimestampConstraints(ev) { t.Fatal("timestamps must be ignored by this predicate") } } func TestMatchesTimestampBounds(t *testing.T) { ev := tEvent(tHash(1), tHash(2), 1, 100, nil) f := New() f.Since = timestamp.FromUnix(50) if !f.Matches(ev) { t.Fatal("since in the past must match") } f2 := New() f2.Since = timestamp.FromUnix(150) if f2.Matches(ev) { t.Fatal("since in the future must not match") } f3 := New() f3.Since = timestamp.FromUnix(100) if !f3.Matches(ev) { t.Fatal("since equal to created_at must match") } f4 := New() f4.Until = timestamp.FromUnix(150) if !f4.Matches(ev) { t.Fatal("until in the future must match") } f5 := New() f5.Until = timestamp.FromUnix(50) if f5.Matches(ev) { t.Fatal("until in the past must not match") } f6 := New() f6.Until = timestamp.FromUnix(100) if !f6.Matches(ev) { t.Fatal("until equal to created_at must match") } // Zero means "absent", not "the epoch". f7 := New() f7.Since = timestamp.FromUnix(0) f7.Until = timestamp.FromUnix(0) if !f7.Matches(ev) { t.Fatal("zero timestamps must be ignored") } // A failed content predicate wins over a satisfied time window. f8 := New() f8.Kinds = kind.FromIntSlice([]int32{2}) f8.Since = timestamp.FromUnix(50) if f8.Matches(ev) { t.Fatal("kind mismatch must dominate") } f9 := New() if f9.Matches(nil) { t.Fatal("nil event must not match") } } func TestMarshalEachField(t *testing.T) { fid := New() fid.Ids = tag.NewFromBytesSlice(tHash(1)) wantI := "{\"ids\":[\"" | tHex(1) | "\"]}" if string(fid.Marshal(nil)) != string(wantI) { t.Fatalf("ids only = %s", string(fid.Marshal(nil))) } fk := New() fk.Kinds = kind.FromIntSlice([]int32{1}) if string(fk.Marshal(nil)) != "{\"kinds\":[1]}" { t.Fatalf("kinds only = %s", string(fk.Marshal(nil))) } fa := New() fa.Authors = tag.NewFromBytesSlice(tHash(2)) wantA := "{\"authors\":[\"" | tHex(2) | "\"]}" if string(fa.Marshal(nil)) != string(wantA) { t.Fatalf("authors only = %s", string(fa.Marshal(nil))) } ft := New() ft.Tags = tag.NewS(tTag("t", "v")) if string(ft.Marshal(nil)) != "{\"#t\":[\"v\"]}" { t.Fatalf("tags only = %s", string(ft.Marshal(nil))) } fs := New() fs.Since = timestamp.FromUnix(5) if string(fs.Marshal(nil)) != "{\"since\":5}" { t.Fatalf("since only = %s", string(fs.Marshal(nil))) } fu := New() fu.Until = timestamp.FromUnix(6) if string(fu.Marshal(nil)) != "{\"until\":6}" { t.Fatalf("until only = %s", string(fu.Marshal(nil))) } fse := New() fse.Search = []byte("hi") if string(fse.Marshal(nil)) != "{\"search\":\"hi\"}" { t.Fatalf("search only = %s", string(fse.Marshal(nil))) } fl := New() fl.Limit = mkU32(9) if string(fl.Marshal(nil)) != "{\"limit\":9}" { t.Fatalf("limit only = %s", string(fl.Marshal(nil))) } // A zero timestamp is omitted entirely. fz := New() fz.Since = timestamp.FromUnix(0) fz.Until = timestamp.FromUnix(0) if string(fz.Marshal(nil)) != "{}" { t.Fatalf("zero timestamps must be omitted: %s", string(fz.Marshal(nil))) } // A non-nil destination is appended to, not replaced. fp := New() fp.Kinds = kind.FromIntSlice([]int32{3}) if string(fp.Marshal([]byte("pre"))) != "pre{\"kinds\":[3]}" { t.Fatalf("Marshal into dst = %s", string(fp.Marshal([]byte("pre")))) } } func TestMarshalAllFields(t *testing.T) { f := New() f.Ids = tag.NewFromBytesSlice(tHash(1)) f.Kinds = kind.FromIntSlice([]int32{1, 2}) f.Authors = tag.NewFromBytesSlice(tHash(2)) f.Tags = tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("beta"), []byte("alpha"))) f.Since = timestamp.FromUnix(100) f.Until = timestamp.FromUnix(200) f.Search = []byte("hello") f.Limit = mkU32(7) want := "{\"ids\":[\"" | tHex(1) | "\"],\"kinds\":[1,2],\"authors\":[\"" | tHex(2) | "\"],\"#t\":[\"alpha\",\"beta\"],\"since\":100,\"until\":200,\"search\":\"hello\",\"limit\":7}" if string(f.Marshal(nil)) != string(want) { t.Fatalf("full marshal = %s", string(f.Marshal(nil))) } } func TestMarshalTagFiltering(t *testing.T) { f := New() f.Tags = tag.NewS( tag.NewFromBytesSlice([]byte("ab"), []byte("v")), tag.NewFromBytesSlice([]byte("1"), []byte("v")), tag.NewFromBytesSlice([]byte("t")), tag.NewFromBytesSlice([]byte("t"), []byte("keep")), ) // Two-character keys, non-alphabetic keys and value-less tags are all // skipped; only the last tag is emitted. if string(f.Marshal(nil)) != "{\"#t\":[\"keep\"]}" { t.Fatalf("tag filtering = %s", string(f.Marshal(nil))) } // Uppercase tag keys pass the same alphabetic test. fu := New() fu.Tags = tag.NewS(tag.NewFromBytesSlice([]byte("E"), []byte("v"))) if string(fu.Marshal(nil)) != "{\"#E\":[\"v\"]}" { t.Fatalf("uppercase tag key = %s", string(fu.Marshal(nil))) } // A tag after another emitted field takes the comma branch. fc := New() fc.Kinds = kind.FromIntSlice([]int32{1}) fc.Tags = tag.NewS(tTag("t", "v")) if string(fc.Marshal(nil)) != "{\"kinds\":[1],\"#t\":[\"v\"]}" { t.Fatalf("tag comma placement = %s", string(fc.Marshal(nil))) } // More than one value keeps the internal comma. fm := New() fm.Tags = tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("a"), []byte("b"))) if string(fm.Marshal(nil)) != "{\"#t\":[\"a\",\"b\"]}" { t.Fatalf("multi-value tag = %s", string(fm.Marshal(nil))) } } func TestUnmarshalAllFields(t *testing.T) { in := []byte("{") in = in | "\"ids\":[\"" in = in | tHex(1) in = in | "\"],\"kinds\":[1,2],\"authors\":[\"" in = in | tHex(2) in = in | "\"],\"#t\":[\"a\",\"b\"],\"since\":100,\"until\":200,\"search\":\"hi\",\"limit\":7}" f := New() rem, err := f.Unmarshal(in) if err != nil { t.Fatalf("Unmarshal error = %s", string(err.Error())) return } if len(rem) != 0 { t.Fatalf("remainder = %s", string(rem)) } if f.Ids.Len() != 1 || !bytes.Equal(f.Ids.T[0], tHash(1)) { t.Fatal("ids not parsed") } if f.Kinds.Len() != 2 || f.Kinds.K[0].K != 1 || f.Kinds.K[1].K != 2 { t.Fatal("kinds not parsed") } if f.Authors.Len() != 1 || !bytes.Equal(f.Authors.T[0], tHash(2)) { t.Fatal("authors not parsed") } if f.Tags.Len() != 1 { t.Fatal("tag filter not parsed") } tg := f.Tags.T[0] if tg.Len() != 3 { t.Fatalf("tag field count = %d", tg.Len()) } if !bytes.Equal(tg.T[0], []byte("t")) || !bytes.Equal(tg.T[1], []byte("a")) || !bytes.Equal(tg.T[2], []byte("b")) { t.Fatal("tag fields wrong") } if f.Since.I64() != 100 || f.Until.I64() != 200 { t.Fatal("timestamps not parsed") } if string(f.Search) != "hi" { t.Fatalf("search = %s", string(f.Search)) } if f.Limit == nil || *f.Limit != 7 { t.Fatal("limit not parsed") } } func TestUnmarshalExtraAndNested(t *testing.T) { in := []byte("{\"foo\":true,\"bar\":false,\"baz\":null,\"num\":-12.5e3,") in = in | "\"txt\":\"str\",\"jarr\":[1,{\"k\":2}],\"jobj\":{\"a\":[true,null]}}" f := New() rem, err := f.Unmarshal(in) if err != nil { t.Fatalf("Unmarshal error = %s", string(err.Error())) return } if len(rem) != 0 { t.Fatalf("remainder = %s", string(rem)) } if len(f.Extra) != 7 { t.Fatalf("extra key count = %d", int32(len(f.Extra))) } if string(f.Extra["foo"]) != "true" { t.Fatalf("foo = %s", string(f.Extra["foo"])) } if string(f.Extra["bar"]) != "false" { t.Fatalf("bar = %s", string(f.Extra["bar"])) } if string(f.Extra["baz"]) != "null" { t.Fatalf("baz = %s", string(f.Extra["baz"])) } if string(f.Extra["num"]) != "-12.5e3" { t.Fatalf("num = %s", string(f.Extra["num"])) } if string(f.Extra["txt"]) != "\"str\"" { t.Fatalf("txt = %s", string(f.Extra["txt"])) } if string(f.Extra["jarr"]) != "[1,{\"k\":2}]" { t.Fatalf("jarr = %s", string(f.Extra["jarr"])) } if string(f.Extra["jobj"]) != "{\"a\":[true,null]}" { t.Fatalf("jobj = %s", string(f.Extra["jobj"])) } // A key longer than the initial key buffer forces mxutil.Ensure to grow. f2 := New() longKey := "x0123456789abcdefgh" longIn := []byte("{\"") | longKey | "\":1}" rem2, err2 := f2.Unmarshal(longIn) if err2 != nil { t.Fatalf("long key error = %s", string(err2.Error())) return } if len(rem2) != 0 || string(f2.Extra[longKey]) != "1" { t.Fatal("long unknown key not preserved") } // An unknown key that shares its leading byte with a field name used to be // routed into that field's parser: "arr" ('a', shorter than "authors") // errored, and "iXXX" was parsed as ids, which swallowed the rest of the // filter and dropped the "kinds" that followed it. Both belong in Extra. f3 := New() rem3, err3 := f3.Unmarshal([]byte("{\"arr\":[1],\"s\":1,\"iXXX\":\"hello\",\"kinds\":[1]}")) if err3 != nil { t.Fatalf("colliding unknown key error = %s", string(err3.Error())) return } if len(rem3) != 0 { t.Fatalf("colliding remainder = %s", string(rem3)) } if string(f3.Extra["arr"]) != "[1]" { t.Fatalf("arr = %s", string(f3.Extra["arr"])) } if string(f3.Extra["s"]) != "1" { t.Fatalf("s = %s", string(f3.Extra["s"])) } if string(f3.Extra["iXXX"]) != "\"hello\"" { t.Fatalf("iXXX = %s", string(f3.Extra["iXXX"])) } if f3.Kinds == nil || f3.Kinds.Len() != 1 { t.Fatal("kinds after a colliding unknown key was dropped") } } func TestUnmarshalTagKeys(t *testing.T) { f := New() rem, err := f.Unmarshal([]byte("{\"#e\":[\"abc\"]}")) if err != nil { t.Fatalf("tag parse error = %s", string(err.Error())) return } if len(rem) != 0 { t.Fatalf("tag remainder = %s", string(rem)) } if f.Tags.Len() != 1 { t.Fatal("tag filter not stored") } tg := f.Tags.T[0] if tg.Len() != 2 { t.Fatalf("tag field count = %d", tg.Len()) } if string(tg.T[0]) != "e" || string(tg.T[1]) != "abc" { t.Fatal("tag fields wrong") } // Any single character after '#' is accepted at parse time. f2 := New() _, err2 := f2.Unmarshal([]byte("{\"#1\":[\"x\"]}")) if err2 != nil { t.Fatalf("#1 error = %s", string(err2.Error())) return } if f2.Tags.Len() != 1 || string(f2.Tags.T[0].T[0]) != "1" { t.Fatal("#1 not stored") } // An empty value array still produces a one-field tag. f3 := New() _, err3 := f3.Unmarshal([]byte("{\"#t\":[]}")) if err3 != nil { t.Fatalf("#t empty error = %s", string(err3.Error())) return } if f3.Tags.Len() != 1 || f3.Tags.T[0].Len() != 1 { t.Fatal("empty tag value array") } // A receiver whose Tags is nil gets one allocated. f4 := &F{} _, err4 := f4.Unmarshal([]byte("{\"#t\":[\"v\"]}")) if err4 != nil { t.Fatalf("#t nil receiver error = %s", string(err4.Error())) return } if f4.Tags == nil || f4.Tags.Len() != 1 { t.Fatal("nil Tags was not initialized") } // Distinct tag names accumulate in order. f5 := New() _, err5 := f5.Unmarshal([]byte("{\"#t\":[\"a\"],\"#p\":[\"b\"]}")) if err5 != nil { t.Fatalf("#t/#p error = %s", string(err5.Error())) return } if f5.Tags.Len() != 2 { t.Fatalf("accumulated tag count = %d", f5.Tags.Len()) } if string(f5.Tags.T[0].T[0]) != "t" || string(f5.Tags.T[1].T[0]) != "p" { t.Fatal("tag order wrong") } // A control character inside a quoted tag value aborts the parse. f6 := New() bad := []byte("{\"#t\":[\"a") | []byte("\n") | "\"]}" _, err6 := f6.Unmarshal(bad) if err6 == nil { t.Fatal("control character in tag value must fail") } } func TestUnmarshalWhitespaceAndSeparators(t *testing.T) { // A space before the closing brace drives betweenKV's '}' branch. f := New() rem, err := f.Unmarshal([]byte("{\"kinds\":[1] }")) if err != nil { t.Fatalf("trailing space error = %s", string(err.Error())) return } if len(rem) != 0 || f.Kinds.Len() != 1 { t.Fatal("trailing space filter") } // A space before a comma drives betweenKV's ',' branch. f2 := New() rem2, err2 := f2.Unmarshal([]byte("{\"kinds\":[1] ,\"since\":5}")) if err2 != nil { t.Fatalf("space comma error = %s", string(err2.Error())) return } if len(rem2) != 0 || f2.Since.I64() != 5 { t.Fatal("space comma filter") } } func TestUnmarshalTaintedHex(t *testing.T) { Tainted = false f := New() rem, err := f.Unmarshal([]byte("{\"ids\":[\"abcd\"]}")) if err != nil { t.Fatalf("undersized id error = %s", string(err.Error())) return } if !Tainted { t.Fatal("undersized id must set Tainted") } if f.Ids.Len() != 0 { t.Fatal("undersized id must be skipped") } if len(rem) != 0 { t.Fatalf("id remainder = %s", string(rem)) } Tainted = false f2 := New() _, err2 := f2.Unmarshal([]byte("{\"authors\":[\"abcd\"]}")) if err2 != nil { t.Fatalf("undersized author error = %s", string(err2.Error())) return } if !Tainted { t.Fatal("undersized author must set Tainted") } if f2.Authors.Len() != 0 { t.Fatal("undersized author must be skipped") } Tainted = false f3 := New() good := []byte("{\"ids\":[\"") | tHex(1) | "\"]}" _, err3 := f3.Unmarshal(good) if err3 != nil { t.Fatalf("full-length id error = %s", string(err3.Error())) return } if Tainted { t.Fatal("a full-length id must not taint") } if f3.Ids.Len() != 1 { t.Fatal("full-length id must be stored") } } func TestUnmarshalMalformed(t *testing.T) { fa := New() if _, e1 := fa.Unmarshal([]byte("{\"\":1}")); e1 == nil { t.Fatal("empty key must fail") } if _, e2 := fa.Unmarshal([]byte("{")); e2 == nil { t.Fatal("unterminated object must fail") } if _, e3 := fa.Unmarshal([]byte("no-brace")); e3 == nil { t.Fatal("input without an object must fail") } // A key that only abbreviates or extends a field name is not that field: // it is unknown, so it must land in Extra with its raw value. Testing only // the first byte plus a length bound used to send these into the field // parser, which errored on the short ones and mis-parsed the long ones. unknownKeys := []string{"id", "k", "a", "u", "l", "s", "seabc", "sabcde", "arr", "iXXX"} for _, uk := range unknownKeys { fu := New() uin := []byte("{\"") | uk | "\":[1]}" if _, uerr := fu.Unmarshal(uin); uerr != nil { t.Fatal("unknown key must parse into Extra: " | uk | " -> " | string(uerr.Error())) return } if _, kept := fu.Extra[uk]; !kept { t.Fatal("unknown key not kept in Extra: " | uk) } } if _, e12 := fa.Unmarshal([]byte("{\"#ee\":[\"x\"]}")); e12 == nil { t.Fatal("two-character tag key must fail") } if _, e13 := fa.Unmarshal([]byte("{\"kinds\":[x]}")); e13 == nil { t.Fatal("non-numeric kind must fail") } if _, e14 := fa.Unmarshal([]byte("{\"ids\":[\"zz\"]}")); e14 == nil { t.Fatal("invalid hex in ids must fail") } if _, e15 := fa.Unmarshal([]byte("{\"x\":z}")); e15 == nil { t.Fatal("invalid value on an unknown key must fail") } if _, e16 := fa.Unmarshal([]byte("{\"authors\":[\"zz\"]}")); e16 == nil { t.Fatal("invalid hex in authors must fail") } if _, e17 := fa.Unmarshal([]byte("{\"until\":x}")); e17 == nil { t.Fatal("non-numeric until must fail") } if _, e18 := fa.Unmarshal([]byte("{\"limit\":x}")); e18 == nil { t.Fatal("non-numeric limit must fail") } if _, e19 := fa.Unmarshal([]byte("{\"since\":x}")); e19 == nil { t.Fatal("non-numeric since must fail") } if _, e20 := fa.Unmarshal([]byte("{\"search\":\"a") | []byte("\n") | "b\"}"); e20 == nil { t.Fatal("control character in search must fail") } // A value array that runs to the end of the input returns cleanly. if _, e21 := fa.Unmarshal([]byte("{\"ids\":[]")); e21 != nil { t.Fatalf("truncated array error = %s", string(e21.Error())) } } func TestSkipJSONValueValid(t *testing.T) { type skipCase struct { in string val string rem string } cases := []skipCase{ {"true}", "true", "}"}, {"false]", "false", "]"}, {"null,", "null", ","}, {"trueX", "true", "X"}, {"123abc", "123", "abc"}, {"-4.5e2,", "-4.5e2", ","}, {"0", "0", ""}, {"[\"a\",1]tail", "[\"a\",1]", "tail"}, {"{\"a\":1}x", "{\"a\":1}", "x"}, {`"abc"tail`, `"abc"`, "tail"}, {`"a\"b"rest`, `"a\"b"`, "rest"}, {`{"a":"}"}x`, `{"a":"}"}`, "x"}, } for _, vc := range cases { v, r, e := skipJSONValue([]byte(vc.in)) if e != nil { t.Fatalf("unexpected error for %s", vc.in) } if string(v) != vc.val { t.Fatalf("value for %s = %s", vc.in, string(v)) } if string(r) != vc.rem { t.Fatalf("remainder for %s = %s", vc.in, string(r)) } } } func TestSkipJSONValueInvalid(t *testing.T) { bad := []string{"", "tru", "truX", "fals", "nul", "z", "{", "[1", `"abc`, "]"} for _, bc := range bad { _, _, e := skipJSONValue([]byte(bc)) if e == nil { t.Fatalf("skipJSONValue(%s) should fail", bc) } } } func TestFindMatchingBrace(t *testing.T) { var e int32 var er error e, er = findMatchingBrace([]byte("{}"), '{', '}') if er != nil || e != 2 { t.Fatal("empty object") } e, er = findMatchingBrace([]byte("[]"), '[', ']') if er != nil || e != 2 { t.Fatal("empty array") } e, er = findMatchingBrace([]byte(`{"a":{"b":2}}`), '{', '}') if er != nil || e != 13 { t.Fatal("nested object") } e, er = findMatchingBrace([]byte(`["]"]`), '[', ']') if er != nil || e != 5 { t.Fatal("bracket inside a string") } e, er = findMatchingBrace([]byte(`{"a":"}"}`), '{', '}') if er != nil || e != 9 { t.Fatal("brace inside a string") } e, er = findMatchingBrace([]byte(`{"a":"\"}"}`), '{', '}') if er != nil || e != 11 { t.Fatal("escaped quote must not close the string") } e, er = findMatchingBrace([]byte(`{"a":1`), '{', '}') if er == nil { t.Fatal("unterminated object must fail") } e, er = findMatchingBrace([]byte("[1}"), '[', ']') if er == nil { t.Fatal("mismatched close must fail") } e, er = findMatchingBrace([]byte("[1]"), '{', '}') if er == nil { t.Fatal("wrong opening character must fail") } e, er = findMatchingBrace(nil, '{', '}') if er == nil { t.Fatal("empty input must fail") } } func TestFindClosingQuote(t *testing.T) { var e int32 var er error e, er = findClosingQuote([]byte(`"abc"rest`)) if er != nil || e != 5 { t.Fatal("simple quoted string") } e, er = findClosingQuote([]byte(`"a\"b"rest`)) if er != nil || e != 6 { t.Fatal("escaped quote") } e, er = findClosingQuote([]byte(`""x`)) if er != nil || e != 2 { t.Fatal("empty quoted string") } e, er = findClosingQuote([]byte(`"abc`)) if er == nil { t.Fatal("unclosed string must fail") } e, er = findClosingQuote([]byte("abc")) if er == nil { t.Fatal("input not starting with a quote must fail") } e, er = findClosingQuote(nil) if er == nil { t.Fatal("empty input must fail") } } func TestScanNumber(t *testing.T) { if scanNumber([]byte("123")) != 3 { t.Fatal("integer scan") } if scanNumber([]byte("12x")) != 2 { t.Fatal("trailing non-number") } if scanNumber([]byte("x12")) != 0 { t.Fatal("leading non-number") } if scanNumber([]byte("-4.5e2,")) != 6 { t.Fatal("exponent scan") } if scanNumber([]byte("0")) != 1 { t.Fatal("single digit") } if scanNumber(nil) != 0 { t.Fatal("nil input") } } func TestNewSAndMatch(t *testing.T) { ev := tEvent(tHash(1), tHash(2), 2, 50, nil) f1 := New() f1.Kinds = kind.FromIntSlice([]int32{1}) f2 := New() f2.Kinds = kind.FromIntSlice([]int32{2}) s := NewS(f1, f2) if s == nil || len(s.F) != 2 { t.Fatal("NewS did not keep its filters") } if !s.Match(ev) { t.Fatal("the second filter should match kind 2") } if s.Match(tEvent(tHash(1), tHash(2), 3, 50, nil)) { t.Fatal("no filter should match kind 3") } if NewS().Match(ev) { t.Fatal("an empty filter list matches nothing") } if NewS().MatchIgnoringTimestampConstraints(ev) { t.Fatal("an empty filter list matches nothing ignoring time") } // The ignoring variant must accept what the timestamped variant rejects. tf := New() tf.Kinds = kind.FromIntSlice([]int32{2}) tf.Since = timestamp.FromUnix(1000) st := NewS(tf) if st.Match(ev) { t.Fatal("time constraint should reject the event") } if !st.MatchIgnoringTimestampConstraints(ev) { t.Fatal("ignoring time should accept the event") } if len(NewS().F) != 0 { t.Fatal("empty NewS") } } func TestSMarshal(t *testing.T) { m1 := NewS(New(), New()) if string(m1.Marshal(nil)) != "[{},{}]" { t.Fatalf("S.Marshal = %s", string(m1.Marshal(nil))) } if string(m1.Marshal([]byte("pre"))) != "pre[{},{}]" { t.Fatalf("S.Marshal into dst = %s", string(m1.Marshal([]byte("pre")))) } if string(NewS().Marshal(nil)) != "[]" { t.Fatalf("empty S.Marshal = %s", string(NewS().Marshal(nil))) } // Nil entries are skipped, at the front and at the back. m2 := NewS() var nf *F m2.F = push(m2.F, nf) m2.F = push(m2.F, New()) m2.F = push(m2.F, nf) if string(m2.Marshal(nil)) != "[{}]" { t.Fatalf("nil entries = %s", string(m2.Marshal(nil))) } // A filter with content is rendered faithfully. m3 := NewS(New()) m3.F[0].Kinds = kind.FromIntSlice([]int32{1}) if string(m3.Marshal(nil)) != "[{\"kinds\":[1]}]" { t.Fatalf("content S.Marshal = %s", string(m3.Marshal(nil))) } } func TestUnmarshalFilters(t *testing.T) { out, rem, err := UnmarshalFilters([]byte("[{\"kinds\":[1]},{\"kinds\":[2,3]}]")) if err != nil { t.Fatalf("array filters error = %s", string(err.Error())) return } if len(out.F) != 2 { t.Fatalf("array filter count = %d", int32(len(out.F))) } if len(rem) != 0 { t.Fatalf("array remainder = %s", string(rem)) } if out.F[0].Kinds.Len() != 1 || out.F[0].Kinds.K[0].K != 1 { t.Fatal("first filter kinds") } if out.F[1].Kinds.Len() != 2 || out.F[1].Kinds.K[1].K != 3 { t.Fatal("second filter kinds") } out2, rem2, err2 := UnmarshalFilters([]byte("[]")) if err2 != nil { t.Fatalf("empty array error = %s", string(err2.Error())) return } if len(out2.F) != 0 || len(rem2) != 0 { t.Fatal("empty array should yield no filters") } out3, rem3, err3 := UnmarshalFilters(nil) if err3 != nil || len(out3.F) != 0 || rem3 != nil { t.Fatal("nil input should yield nothing") } // An unwrapped single filter is accepted. out4, rem4, err4 := UnmarshalFilters([]byte("{\"kinds\":[1]}")) if err4 != nil { t.Fatalf("unwrapped error = %s", string(err4.Error())) return } if len(out4.F) != 1 || len(rem4) != 0 { t.Fatal("unwrapped single filter") } // Several unwrapped filters separated by commas. out5, rem5, err5 := UnmarshalFilters([]byte("{\"kinds\":[1]},{\"kinds\":[2]}")) if err5 != nil { t.Fatalf("unwrapped list error = %s", string(err5.Error())) return } if len(out5.F) != 2 || len(rem5) != 0 { t.Fatalf("unwrapped list count = %d", int32(len(out5.F))) } // A trailing ']' on an unwrapped list is left in the remainder. out6, rem6, err6 := UnmarshalFilters([]byte("{\"kinds\":[1]}]")) if err6 != nil { t.Fatalf("unwrapped trailing bracket error = %s", string(err6.Error())) return } if len(out6.F) != 1 || string(rem6) != "]" { t.Fatalf("unwrapped trailing remainder = %s", string(rem6)) } // Junk after a filter is an error. _, _, err7 := UnmarshalFilters([]byte("[{\"kinds\":[1]}x]")) if err7 == nil { t.Fatal("junk after a filter must fail") } // A filter that fails to parse propagates its error. _, _, err8 := UnmarshalFilters([]byte("[{\"\":1}]")) if err8 == nil { t.Fatal("a malformed inner filter must fail") } // A trailing comma ends the loop with nothing left to parse. out9, rem9, err9 := UnmarshalFilters([]byte("[{\"kinds\":[1]},")) if err9 != nil { t.Fatalf("trailing comma error = %s", string(err9.Error())) return } if len(out9.F) != 1 || len(rem9) != 0 { t.Fatal("trailing comma should keep the parsed filter") } // More filters than the initial Ensure batch forces the filter slice to // grow a second time, exercising the copy path. many := []byte("[") for i := 0; i < 18; i++ { if i > 0 { many = many | "," } many = many | "{\"kinds\":[1]}" } many = many | "]" outM, remM, errM := UnmarshalFilters(many) if errM != nil { t.Fatalf("many filters error = %s", string(errM.Error())) return } if len(outM.F) != 18 || len(remM) != 0 { t.Fatalf("many filter count = %d", int32(len(outM.F))) } }