event typechecker.
This commit is contained in:
175
schema/schema.go
Normal file
175
schema/schema.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"github.com/segmentio/encoding/json"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed schema.yaml
|
||||
var schemaFile []byte
|
||||
|
||||
type Schema map[string]KindSchema
|
||||
|
||||
type KindSchema struct {
|
||||
Content string `yaml:"content"`
|
||||
Tags []tagSpec `yaml:"tags"`
|
||||
}
|
||||
|
||||
type tagSpec struct {
|
||||
Name string `yaml:"name"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Next *nextSpec `yaml:"next"`
|
||||
}
|
||||
|
||||
type nextSpec struct {
|
||||
Type string `yaml:"type"`
|
||||
Required bool `yaml:"required"`
|
||||
Either []string `yaml:"either"`
|
||||
Next *nextSpec `yaml:"next"`
|
||||
Variadic bool `yaml:"variadic"`
|
||||
}
|
||||
|
||||
type Validator struct {
|
||||
Schema Schema
|
||||
FailOnUnknown bool
|
||||
}
|
||||
|
||||
func NewValidator(schemaData string) Validator {
|
||||
schema := make(Schema)
|
||||
if err := yaml.Unmarshal([]byte(schemaData), &schema); err != nil {
|
||||
panic(fmt.Errorf("failed to parse schema.yaml: %w", err))
|
||||
}
|
||||
|
||||
return Validator{Schema: schema}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnknownContent = fmt.Errorf("unknown content")
|
||||
ErrUnknownKind = fmt.Errorf("unknown kind")
|
||||
ErrInvalidContentJson = fmt.Errorf("invalid content json")
|
||||
ErrEmptyTag = fmt.Errorf("empty tag")
|
||||
ErrUnknownTagType = fmt.Errorf("unknown tag type")
|
||||
)
|
||||
|
||||
func (v *Validator) ValidateEvent(evt nostr.Event) error {
|
||||
if sch, ok := v.Schema[strconv.FormatUint(uint64(evt.Kind), 10)]; ok {
|
||||
switch sch.Content {
|
||||
case "json":
|
||||
if !json.Valid(unsafe.Slice(unsafe.StringData(evt.Content), len(evt.Content))) {
|
||||
return ErrInvalidContentJson
|
||||
}
|
||||
case "free":
|
||||
default:
|
||||
if v.FailOnUnknown {
|
||||
return ErrInvalidContentJson
|
||||
}
|
||||
}
|
||||
|
||||
tags:
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) == 0 {
|
||||
return ErrEmptyTag
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
specs:
|
||||
for _, tagspec := range sch.Tags {
|
||||
if tagspec.Name == tag[0] || (tagspec.Prefix != "" && strings.HasPrefix(tag[0], tagspec.Prefix)) {
|
||||
if tagspec.Next != nil {
|
||||
if err := v.validateNext(tag, 1, tagspec.Next); err != nil {
|
||||
lastErr = err
|
||||
continue specs // see if there is another tagspec that matches this
|
||||
} else {
|
||||
continue tags
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue specs
|
||||
}
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
// there was at least one failure for this tag and no further successes
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v.FailOnUnknown {
|
||||
return ErrUnknownKind
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var gitcommitdummydecoder = make([]byte, 20)
|
||||
|
||||
func (v *Validator) validateNext(tag nostr.Tag, index int, this *nextSpec) error {
|
||||
if len(tag) <= index {
|
||||
if this.Required {
|
||||
return fmt.Errorf("invalid tag '%s', missing index %d", tag[0], index)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch this.Type {
|
||||
case "id":
|
||||
if _, err := nostr.IDFromHex(tag[index]); err != nil {
|
||||
return fmt.Errorf("invalid id at tag '%s', index %d", tag[0], index)
|
||||
}
|
||||
case "pubkey":
|
||||
if _, err := nostr.PubKeyFromHex(tag[index]); err != nil {
|
||||
return fmt.Errorf("invalid pubkey at tag '%s', index %d", tag[0], index)
|
||||
}
|
||||
case "addr":
|
||||
if _, err := nostr.ParseAddrString(tag[index]); err != nil {
|
||||
return fmt.Errorf("invalid addr at tag '%s', index %d", tag[0], index)
|
||||
}
|
||||
case "kind":
|
||||
if _, err := strconv.ParseUint(tag[index], 10, 16); err != nil {
|
||||
return fmt.Errorf("invalid kind at tag '%s', index %d", tag[0], index)
|
||||
}
|
||||
case "relay":
|
||||
if url, err := url.Parse(tag[index]); err != nil || (url.Scheme != "ws" && url.Scheme != "wss") {
|
||||
return fmt.Errorf("invalid relay at tag '%s', index %d", tag[0], index)
|
||||
}
|
||||
case "constrained":
|
||||
if !slices.Contains(this.Either, tag[index]) {
|
||||
return fmt.Errorf("invalid constrained at tag '%s', index %d", tag[0], index)
|
||||
}
|
||||
case "gitcommit":
|
||||
if len(tag[index]) != 40 {
|
||||
return fmt.Errorf("invalid gitcommit at tag '%s', index %d", tag[0], index)
|
||||
}
|
||||
if _, err := hex.Decode(gitcommitdummydecoder, unsafe.Slice(unsafe.StringData(tag[index]), 40)); err != nil {
|
||||
return fmt.Errorf("invalid gitcommit at tag '%s', index %d", tag[0], index)
|
||||
}
|
||||
case "free":
|
||||
default:
|
||||
if v.FailOnUnknown {
|
||||
return ErrUnknownTagType
|
||||
}
|
||||
}
|
||||
|
||||
if this.Variadic {
|
||||
// apply this same validation to all further items
|
||||
if len(tag) >= index {
|
||||
return v.validateNext(tag, index+1, this)
|
||||
}
|
||||
}
|
||||
|
||||
if this.Next != nil {
|
||||
return v.validateNext(tag, index+1, this.Next)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user