Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

validate.gno

3.15 Kb · 129 lines
  1package gnotif
  2
  3import "strings"
  4
  5const (
  6	targetPrefix   = "gno.land/r/"
  7	maxTarget      = 256
  8	maxName        = 64
  9	maxFilterPairs = 8
 10	maxBody        = 255
 11	maxLink        = 256
 12	maxPerTarget   = 64
 13)
 14
 15func validate(t Trigger) {
 16	switch {
 17	case !validName(t.Event, 1, maxName):
 18		panic("invalid event")
 19	case !validFilter(t.Filter):
 20		panic("invalid filter")
 21	case !validName(t.Param, 0, maxName):
 22		panic("invalid param")
 23	case !sized(t.Title, 1, maxName):
 24		panic("invalid title")
 25	case !sized(t.Body, 0, maxBody):
 26		panic("invalid body")
 27	case !validLink(t.Link):
 28		panic("invalid link")
 29	}
 30}
 31
 32// namespaceOf returns the namespace a valid target lives in, and panics on a
 33// target that is not a valid realm path.
 34func namespaceOf(target string) string {
 35	if !validTarget(target) {
 36		panic("invalid target " + target)
 37	}
 38	namespace, _, _ := strings.Cut(target[len(targetPrefix):], "/")
 39	return namespace
 40}
 41
 42func sized(s string, lo, hi int) bool {
 43	return len(s) >= lo && len(s) <= hi
 44}
 45
 46// validTarget accepts a realm path whose segments each match the chain's
 47// Re_name, so a target names a path where a realm can exist.
 48func validTarget(target string) bool {
 49	if !strings.HasPrefix(target, targetPrefix) || len(target) > maxTarget {
 50		return false
 51	}
 52	rest := target[len(targetPrefix):]
 53	start := 0 // index of the current segment's first byte
 54	for i := 0; i <= len(rest); i++ {
 55		if i == len(rest) || rest[i] == '/' {
 56			if i == start || !isLowerAlnum(rest[i-1]) {
 57				return false
 58			}
 59			start = i + 1
 60			continue
 61		}
 62		c := rest[i]
 63		switch {
 64		case i == start:
 65			if !(c >= 'a' && c <= 'z') {
 66				return false
 67			}
 68		case c == '_' || c == '-':
 69			if !isLowerAlnum(rest[i-1]) {
 70				return false
 71			}
 72		case !isLowerAlnum(c):
 73			return false
 74		}
 75	}
 76	return true
 77}
 78
 79func isLowerAlnum(c byte) bool {
 80	return c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
 81}
 82
 83// validName accepts identifier bytes only, so a name cannot carry spaces or
 84// symbols that imitate the verified mark in rendered output.
 85func validName(s string, lo, hi int) bool {
 86	if !sized(s, lo, hi) {
 87		return false
 88	}
 89	for i := 0; i < len(s); i++ {
 90		c := s[i]
 91		if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_') {
 92			return false
 93		}
 94	}
 95	return true
 96}
 97
 98func validFilter(filter string) bool {
 99	if filter == "" {
100		return true
101	}
102	pairs := strings.Split(filter, ",")
103	if len(pairs) > maxFilterPairs {
104		return false
105	}
106	for _, pair := range pairs {
107		key, value, ok := strings.Cut(pair, "=")
108		if !ok || !validName(key, 1, maxName) || !sized(value, 1, maxName) || strings.Contains(value, "=") {
109			return false
110		}
111	}
112	return true
113}
114
115// validLink accepts only a path, so a click always stays on the dapp's origin.
116// "//host" starts with a slash yet resolves to another host. A URL parser
117// drops tab, LF and CR and reads a backslash as a slash, so "/\t/host" and
118// "/\host" would do the same; control bytes and backslashes are refused.
119func validLink(link string) bool {
120	if !sized(link, 1, maxLink) || !strings.HasPrefix(link, "/") || strings.HasPrefix(link, "//") {
121		return false
122	}
123	for i := 0; i < len(link); i++ {
124		if c := link[i]; c < 0x20 || c == 0x7f || c == '\\' {
125			return false
126		}
127	}
128	return true
129}